宁波网站搭建,wordpress超人采集侠,龙华网站建设深圳信科,教做网站的学校 Java Agent是一种特殊的Java程序#xff0c;它可以在JVM启动时或运行时动态加载#xff0c;用于监控和修改其他Java应用程序的行为。通过Java Agent#xff0c;开发者可以在不修改目标应用程序源码的情况下#xff0c;动态地插入功能#xff0c;如性能分析、日志记录… Java Agent是一种特殊的Java程序它可以在JVM启动时或运行时动态加载用于监控和修改其他Java应用程序的行为。通过Java Agent开发者可以在不修改目标应用程序源码的情况下动态地插入功能如性能分析、日志记录、代码覆盖率测试、热更新等。
一、Java Agent的主要功能
1、监控类的加载在类加载到JVM时可以对类进行操作例如记录日志、统计加载时间。2、修改类的字节码在类被加载时可以修改其字节码例如插入调试代码、改变类的方法行为。3、重新定义已加载的类在程序运行时可以重新定义已经加载的类需要JVM支持。4、监控和获取对象的内存信息可以获取对象的大小用于内存分析。二、Java Agent的实现方式
1、JVM启动时加载在启动Java应用程序时通过-javaagent参数加载。这种方式会在目标应用启动前执行可以拦截所有类的加载过程2、运行时动态附加在应用程序已经启动的情况下通过附加到目标JVM进程来加载。这需要Java提供的Attach API三、Java Agent的历史背景和具体应用场景
Java Agent功能是JDK1.5引入的通过java.lang.instrument接口实现。这个接口基于JVMTIJava Virtual Machine Tool Interface机制允许开发者构建一个独立于应用程序的代理程序用于监测和协助运行在JVM上的程序四、示例
示例1静态加载方式启动执行
工程1 agent 步骤1:pom文件
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdorg.example/groupIdartifactIddemo-javaagent/artifactIdversion1.0/versionpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingproject.reporting.outputEncodingUTF-8/project.reporting.outputEncodingmaven.compiler.source8/maven.compiler.sourcemaven.compiler.target8/maven.compiler.target/propertiesdependenciesdependencygroupIdorg.javassist/groupIdartifactIdjavassist/artifactIdversion3.25.0-GA/version/dependency/dependenciesbuildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-assembly-plugin/artifactIdversion3.1.1/versionconfigurationdescriptorRefs!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包jar 包会被解压开平铺到最终的 uber-jar 里去。输出格式为 jar--descriptorRefjar-with-dependencies/descriptorRef/descriptorRefsarchive!-- 设置manifest配置文件--manifestEntries!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。--Premain-Classdemo.MethodAgentMain/Premain-Class!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。--Agent-Classdemo.MethodAgentMain/Agent-Class!--Can-Redefine-Classes: 是否可进行类定义。--Can-Redefine-Classestrue/Can-Redefine-Classes!--Can-Retransform-Classes: 是否可进行类转换。--Can-Retransform-Classestrue/Can-Retransform-Classes/manifestEntries/archive/configurationexecutionsexecution!--绑定到package生命周期阶段上--phasepackage/phasegoals!--绑定到package生命周期阶段上--goalsingle/goal/goals/execution/executions/pluginplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.1/versionconfigurationsource${maven.compiler.source}/sourcetarget${maven.compiler.target}/target/configuration/plugin/plugins/build
/project步骤2: 创建 premain 方法方法的主要功能是修改 App setName() 方法体
package demo;import java.lang.instrument.Instrumentation;public class MethodAgentMain {public static void premain(String args, Instrumentation inst) {MyTransformer tran new MyTransformer();inst.addTransformer(tran);}
}package demo;import javassist.*;import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer {Overridepublic byte[] transform(ClassLoader loader, String className, Class? classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if (org/example/App.equals(className)) {try {// 从ClassPool获得CtClass对象final ClassPool classPool ClassPool.getDefault();// 尝试添加额外的类路径如果需要classPool.appendClassPath(new ClassClassPath(this.getClass()));classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));final CtClass clazz classPool.get(org.example.App);CtMethod convertToAbbr clazz.getDeclaredMethod(setName);String methodBody {\n this.name \ccc\ \ aaa\;\n };convertToAbbr.setBody(methodBody);byte[] byteCode clazz.toBytecode();clazz.detach();return byteCode;} catch (NotFoundException | CannotCompileException | IOException e) {e.printStackTrace();}}System.out.println(className);return classfileBuffer;}}步骤3: 编译打包 执行 mvn clean package 编译打包最终打包生成了 agent jar 包结果示例
工程2主工程
package org.example;public class App {private int code;private String name;public int getCode() {return code;}public void setCode(int code) {this.code code;}public String getName() {return name;}public void setName(String name) {this.name name;}Overridepublic String toString() {return App{ code code , name name \ };}
}package org.example;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;SpringBootApplication
public class DemoApplication {public static void main(String[] args) {App app new App();app.setName(a);app.setCode(123);System.out.println(app);SpringApplication.run(DemoApplication.class, args);}
}启动 javaagent
java -javaagent:demo-javaagent-1.0-jar-with-dependencies.jar -jar demo-1.0.0-SNAPSHOT.jar output.log查看运行结果
App{code123, nameccc aaa}name 属性被成功修改。
示例2动态加载方式启动之后接口调用触发
在接口调用时触发某些行为可以使用 Java Agent 来改变接口方法调用的行为工程1 agent 步骤1:pom文件
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdorg.example/groupIdartifactIddemo-javaagent/artifactIdversion1.0/versionpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingproject.reporting.outputEncodingUTF-8/project.reporting.outputEncodingmaven.compiler.source8/maven.compiler.sourcemaven.compiler.target8/maven.compiler.target/propertiesdependenciesdependencygroupIdorg.javassist/groupIdartifactIdjavassist/artifactIdversion3.25.0-GA/version/dependency/dependenciesbuildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-assembly-plugin/artifactIdversion3.1.1/versionconfigurationdescriptorRefs!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包jar 包会被解压开平铺到最终的 uber-jar 里去。输出格式为 jar--descriptorRefjar-with-dependencies/descriptorRef/descriptorRefsarchive!-- 设置manifest配置文件--manifestEntries!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。--Premain-Classdemo.MethodAgentMain/Premain-Class!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。--Agent-Classdemo.MethodAgentMain/Agent-Class!--Can-Redefine-Classes: 是否可进行类定义。--Can-Redefine-Classestrue/Can-Redefine-Classes!--Can-Retransform-Classes: 是否可进行类转换。--Can-Retransform-Classestrue/Can-Retransform-Classes/manifestEntries/archive/configurationexecutionsexecution!--绑定到package生命周期阶段上--phasepackage/phasegoals!--绑定到package生命周期阶段上--goalsingle/goal/goals/execution/executions/pluginplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.1/versionconfigurationsource${maven.compiler.source}/sourcetarget${maven.compiler.target}/target/configuration/plugin/plugins/build
/project步骤2: 创建 premain 方法方法的主要功能是修改方法体在方法调用前后添加日志输出
package demo;import java.lang.instrument.Instrumentation;public class MethodAgentMain {public static void premain(String args, Instrumentation inst) {MyTransformer tran new MyTransformer();inst.addTransformer(tran);}
}package demo;import javassist.*;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer {Overridepublic byte[] transform(ClassLoader loader, String className, Class? classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {if (className ! null className.startsWith(org/example/controller/)) {ClassPool pool ClassPool.getDefault();// 尝试添加额外的类路径如果需要pool.appendClassPath(new ClassClassPath(this.getClass()));pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));CtClass ctClass;try {ctClass pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));for (CtBehavior method : ctClass.getDeclaredBehaviors()) {if (method.isEmpty() || method.getMethodInfo() null) {continue;}// 修改方法体在方法调用前后添加日志输出method.insertBefore(System.out.println(\Before method call: \ $sig););method.insertAfter(System.out.println(\After method call: \ $sig););}return ctClass.toBytecode();} catch (Exception e) {e.printStackTrace();}}return null;}}步骤3: 编译打包 执行 mvn clean package 编译打包最终打包生成了 agent jar 包结果示例 工程2主工程 一个普通的spring工程
package org.example.controller;import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** Description: TODO* Author: Top* Version: V1.0* Date: 2020-01-15 15:03*/
RestController
RequestMapping(/api/{edition})
public class ConsumerController {Autowiredprivate Environment env;GetMapping(/detail)ResponseBodypublic String detail(String name) throws JsonProcessingException {return name;}启动工程
java -javaagent:demo-javaagent-1.0-jar-with-dependencies.jar -jar demo-1.0.0-SNAPSHOT.jar output.log执行结果
Before method call: [Ljava.lang.Class;bab9ac
consumer
After method call: [Ljava.lang.Class;2756b30eJava agent原理说明
主流的JVM都提供了Instrumentation的实现但是鉴于Instrumentation的特殊功能并不适合直接提供在JDK的runtime里而更适合出现在Java程序的外层以上帝视角在合适的时机出现。
因此如果想使用Instrumentation功能拿到Instrumentation实例我们必须通过Java agent。
Java agent是一种特殊的Java程序Jar文件它是Instrumentation的客户端。与普通Java程序通过main方法启动不同agent 并不是一个可以单独启动的程序而必须依附在一个Java应用程序JVM上与它运行在同一个进程中通过Instrumentation API与虚拟机交互。
Java agent与Instrumentation密不可分二者也需要在一起使用。因为Instrumentation的实例会作为参数注入到Java agent的启动方法中。Instrumentation是Java提供的JVM接口该接口提供了一系列查看和操作Java类定义的方法例如修改类的字节码、向 classLoader 的 classpath 下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态进而实现Java程序的监控分析甚至实现一些特殊功能如AOP、热部署。
public interface Instrumentation {/*** 注册一个Transformer从此之后的类加载都会被Transformer拦截。* Transformer可以直接对类的字节码byte[]进行修改*/void addTransformer(ClassFileTransformer transformer);/*** 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。* retransformClasses可以修改方法体但是不能变更方法签名、增加和删除方法/类的成员属性*/void retransformClasses(Class?... classes) throws UnmodifiableClassException;/*** 获取一个对象的大小*/long getObjectSize(Object objectToSize);/*** 将一个jar加入到bootstrap classloader的 classpath里*/void appendToBootstrapClassLoaderSearch(JarFile jarfile);/*** 获取当前被JVM加载的所有类对象*/Class[] getAllLoadedClasses();
}
注意 Instrumentation的局限性
在运行时我们可以通过Instrumentation的redefineClasses方法进行类重定义在redefineClasses方法上有一段注释需要特别注意
java 代码解读复制代码 * The redefinition may change method bodies, the constant pool and attributes.* The redefinition must not add, remove or rename fields or methods, change the* signatures of methods, or change inheritance. These restrictions maybe be* lifted in future versions. The class file bytes are not checked, verified and installed* until after the transformations have been applied, if the resultant bytes are in* error this method will throw an exception.这里面提到我们不可以增加、删除或者重命名字段和方法改变方法的签名或者类的继承关系。认识到这一点很重要当我们通过ASM获取到增强的字节码之后如果增强后的字节码没有遵守这些规则那么调用redefineClasses方法来进行类的重定义就会失败。