一、Javaagent 是什么Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包并且对该 java 包有2个要求这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。Premain-Class 指定的那个类必须实现 premain() 方法。premain 方法从字面上理解就是运行在 main 函数之前的的类。当Java 虚拟机启动时在执行 main 函数之前JVM会先运行-javaagent所指定jar 包内Premain-Class 这个类的 premain 方法 。在命令行输入java可以看到相应的参数其中有 和 java agent相关的在上面-javaagent参数中提到了参阅java.lang.instrument这是在rt.jar中定义的一个包该路径下有两个重要的类该包提供了一些工具帮助开发人员在 Java 程序运行时动态修改系统中的 Class 类型。其中使用该软件包的一个关键组件就是 Javaagent。从名字上看似乎是个 Java 代理之类的而实际上他的功能更像是一个Class 类型的转换器他可以在运行时接受重新外部请求对Class类型进行修改。从本质上讲Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法并且对premain方法的签名也有要求签名必须满足以下两种格式public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)JVM 会优先加载 带Instrumentation签名的方法加载成功忽略第二种如果第一种没有则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中截图是jdk17Instrumentation 类 定义如下public interface Instrumentation { //增加一个Class 文件的转换器转换器用于改变 Class 二进制流的数据参数 canRetransform 设置是否允许重新转换。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在类加载之前重新定义 Class 文件ClassDefinition 表示对一个类新的定义如果在类加载之后需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后后续的类加载都会被Transformer拦截。对于已经加载过的类可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后除非再次被retransform否则不会恢复。 void addTransformer(ClassFileTransformer transformer); //删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //在类加载之后重新定义 Class。这个很重要该方法是1.6 之后加入的事实上该方法是 update 了一个类。 void retransformClasses(Class?... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported(); void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class? theClass); SuppressWarnings(rawtypes) Class[] getAllLoadedClasses(); SuppressWarnings(rawtypes) Class[] getInitiatedClasses(ClassLoader loader); //获取一个对象的大小 long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }最为重要的是上面注释的几个方法下面我们会用到。二、如何使用javaagent使用 javaagent 需要几个步骤定义一个 MANIFEST.MF 文件必须包含 Premain-Class 选项通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。创建一个Premain-Class 指定的类类中包含 premain 方法方法逻辑由用户自己确定。将 premain 的类和 MANIFEST.MF 文件打成 jar 包。使用参数 -javaagent: jar包路径 启动要代理的方法。在执行以上步骤后JVM 会先执行 premain 方法大部分类加载都会通过该方法注意是大部分不是所有。当然遗漏的主要是系统类因为很多系统类先于 agent 执行而用户类的加载肯定是会被拦截的。也就是说这个方法是在 main 方法启动前拦截大部分类的加载活动既然可以拦截类的加载那么就可以去做重写类这样的操作结合第三方的字节码编译工具比如ASMjavassistcglib等等来改写实现类。通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程一个工程是用来承载 javaagent类单独的打成jar包一个工程是javaagent需要去代理的类。即javaagent会在这个工程中的main方法启动之前去做一些事情。2.1、首先来实现javaagent工程工程目录结构如下2.2、添加javassist依赖包dependency groupIdorg.javassist/groupId artifactIdjavassist/artifactId version3.30.2-GA/version /dependency2.3、创建一个一个带Instrumentation参数的premain()方法package org.example; import java.lang.instrument.Instrumentation; public class Agent { public static void premain(String agentArgs, Instrumentation inst){ System.out.println(agent premain method); inst.addTransformer(new MyTransformer(agentArgs)); } }2.4、创建一个类实现ClassFileTransformer接口的transform(......)方法。byte[] transform(ClassLoader loader, String className, Class? classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)package org.example; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; public class MyTransformer implements ClassFileTransformer { //指定匹配报名(方便测试) private String packageName; MyTransformer(String packageName) { // 从参数中获取指定的包名 System.out.println(Load successfully from args! Package name: packageName); // 将包名保存到成员变量中 this.packageName packageName; } /** * param loader 类加载器 * param className 类名 * param classBeingRedefined 重定义的类 * param protectionDomain 保护域 * param classfileBuffer 类文件字节码 * return 修改后的类字节码返回null则表示不修改 */ Override public byte[] transform(ClassLoader loader, String className, Class? classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { className className.replace(/, .); //这里我们限制下只针对目标报下进行耗时统计 if (!className.startsWith(packageName)) { //System.out.println(Transforming class: className); return classfileBuffer; } //ClassPool:字节码类的缓存池管理所有 CtClass ClassPool classPool ClassPool.getDefault(); try { //将类文件字节码转换成CtClass对象 CtClass ctClass classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); //获取类中所有方法 CtMethod[] declaredMethods ctClass.getDeclaredMethods(); for (CtMethod method : declaredMethods) { //给方法添加一个 long 类型的局部变量 start局部变量需要通过“addLocalVariable”类声明局部变量 method.addLocalVariable(start, CtClass.longType); //在方法体最开头插入赋值语句记录方法开始执行的时间戳 method.insertBefore(start System.currentTimeMillis();); //获取方法全限定名 String methodLongName method.getLongName(); //在方法体末尾插入打印语句用当前时间减去 start 得到耗时 method.insertAfter(System.out.println(\ methodLongName \ \ takes \ (System.currentTimeMillis() - start) \ms\);); } //将CtClass对象转换成字节码 byte[] bytecode ctClass.toBytecode(); return bytecode; } catch (IOException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } return classfileBuffer; } }2.5、修改pom.xml文件pom.xml文件添加maven-assembly-plugin插件。build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId version3.7.1/version configuration !--使用内置的 jar-with-dependencies 描述符打包时会把所有依赖如 javassist的 class 文件一并打入 JAR 生成一个 fat jar。最终产出类似 java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar。 名词fat jar解释:Fat JAR也叫 Uber JAR是指把项目自身的 class 文件和所有依赖的第三方库 class 文件全部打包进一个 JAR 文件中。 -- descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs archive !--自动添加META-INF/MANIFEST.MF -- manifest addClasspathtrue/addClasspath /manifest manifestEntries !--JVM 运行中动态附加的 Agent 入口类通过 Attach API 使用-- Agent-Classorg.example.Agent/Agent-Class !--JVM 启动时加载的 Agent 入口类通过 -javaagent 参数使用-- Premain-Classorg.example.Agent/Premain-Class !--允许 Agent 重新定义类替换整个类的字节码-- Can-Redefine-Classestrue/Can-Redefine-Classes !--允许 Agent 重新转换已加载的类通过 ClassFileTransformer 修改字节码-- Can-Retransform-Classestrue/Can-Retransform-Classes /manifestEntries /archive /configuration executions execution idmake-assembly/id !--打包时执行assembly插件的single目标-- phasepackage/phase goals goalsingle/goal /goals /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.1.0/version /plugin /plugins /build2.6、使用package命令打包成一个代理jar包在项目的target里可以找到一个“Fat jar”。名词fat jar解释: Fat JAR也叫 Uber JAR是指把项目自身的 class 文件和所有依赖的第三方库 class 文件全部打包进一个 JAR 文件中找到这个jar所在文件目录地址使用解压工具解压后找到META-INF/MANIFEST.MF文件使用记事本打开发现内容如下这是默认的文件包含当前的一些版本信息当前工程的启动类它还有别的参数允许你做更多的事情可以用上的有Manifest-Version: 1.0 Created-By: Maven Archiver 3.6.1 Build-Jdk-Spec: 17 Class-Path: javassist-3.30.2-GA.jar Agent-Class: org.example.Agent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: org.example.AgentPremain-Class 包含 premain 方法的类类的全路径名Agent-Class 包含 agentmain 方法的类类的全路径名Class-Path 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符“/”开头则为绝对路径否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的则忽略不表示 JAR 文件的路径。可选Can-Redefine-Classes true表示能重定义此代理所需的类默认值为 false可选Can-Retransform-Classes true 表示能重转换此代理所需的类默认值为 false 可选三、测试javaagent是否生效3.1、新建一个SpringBoot工程3.2、新建一个web方法package org.example.springbootdemo; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController public class DemoController { /** * 模拟耗时操作 * param cost * return */ RequestMapping(/take/{cost}) public String take(PathVariable(name cost) String cost) { try { Thread.sleep(Integer.parseInt(cost) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } return success; } }3.3、使maven的package命令打包这个SpringBoot工程打完包后使用Terminal终端打开该SpringBoot项目的target目录。3.4、指定代理jar运行jar测试命令如下java -javaagent:D:\projects\personal\spring-cloud-alibaba-framework\spring-cloud-alibaba\java-agent\target\java-agent-1.0-SNAPSHOT-jar-with-dependencies.jarorg.example -jar .\springboot-demo-0.0.1-SNAPSHOT.jar命令解释命令解释javajava命令-javaagent:xx/xx.jarjava代理命令xxx/xx.jar表示代理jar所在的目录地址l路径。注意“xx.jarorg.example”则是我们之前定义类MyTransformer初始化时给属性packageName赋值。-jar xxx.jar指定本次要运行的jar包启动后可以看到MyTransformer类构造函数初始化赋值的日志打印。3.5、web访问测试浏览器分别访问http://localhost:8080/task/1http://localhost:8080/task/2http://localhost:8080/task/3查看控制台的耗时统计。
学习并模拟JavaAgent探针技术
一、Javaagent 是什么Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包并且对该 java 包有2个要求这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。Premain-Class 指定的那个类必须实现 premain() 方法。premain 方法从字面上理解就是运行在 main 函数之前的的类。当Java 虚拟机启动时在执行 main 函数之前JVM会先运行-javaagent所指定jar 包内Premain-Class 这个类的 premain 方法 。在命令行输入java可以看到相应的参数其中有 和 java agent相关的在上面-javaagent参数中提到了参阅java.lang.instrument这是在rt.jar中定义的一个包该路径下有两个重要的类该包提供了一些工具帮助开发人员在 Java 程序运行时动态修改系统中的 Class 类型。其中使用该软件包的一个关键组件就是 Javaagent。从名字上看似乎是个 Java 代理之类的而实际上他的功能更像是一个Class 类型的转换器他可以在运行时接受重新外部请求对Class类型进行修改。从本质上讲Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法并且对premain方法的签名也有要求签名必须满足以下两种格式public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)JVM 会优先加载 带Instrumentation签名的方法加载成功忽略第二种如果第一种没有则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中截图是jdk17Instrumentation 类 定义如下public interface Instrumentation { //增加一个Class 文件的转换器转换器用于改变 Class 二进制流的数据参数 canRetransform 设置是否允许重新转换。 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在类加载之前重新定义 Class 文件ClassDefinition 表示对一个类新的定义如果在类加载之后需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后后续的类加载都会被Transformer拦截。对于已经加载过的类可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后除非再次被retransform否则不会恢复。 void addTransformer(ClassFileTransformer transformer); //删除一个类转换器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //在类加载之后重新定义 Class。这个很重要该方法是1.6 之后加入的事实上该方法是 update 了一个类。 void retransformClasses(Class?... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported(); void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class? theClass); SuppressWarnings(rawtypes) Class[] getAllLoadedClasses(); SuppressWarnings(rawtypes) Class[] getInitiatedClasses(ClassLoader loader); //获取一个对象的大小 long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }最为重要的是上面注释的几个方法下面我们会用到。二、如何使用javaagent使用 javaagent 需要几个步骤定义一个 MANIFEST.MF 文件必须包含 Premain-Class 选项通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。创建一个Premain-Class 指定的类类中包含 premain 方法方法逻辑由用户自己确定。将 premain 的类和 MANIFEST.MF 文件打成 jar 包。使用参数 -javaagent: jar包路径 启动要代理的方法。在执行以上步骤后JVM 会先执行 premain 方法大部分类加载都会通过该方法注意是大部分不是所有。当然遗漏的主要是系统类因为很多系统类先于 agent 执行而用户类的加载肯定是会被拦截的。也就是说这个方法是在 main 方法启动前拦截大部分类的加载活动既然可以拦截类的加载那么就可以去做重写类这样的操作结合第三方的字节码编译工具比如ASMjavassistcglib等等来改写实现类。通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程一个工程是用来承载 javaagent类单独的打成jar包一个工程是javaagent需要去代理的类。即javaagent会在这个工程中的main方法启动之前去做一些事情。2.1、首先来实现javaagent工程工程目录结构如下2.2、添加javassist依赖包dependency groupIdorg.javassist/groupId artifactIdjavassist/artifactId version3.30.2-GA/version /dependency2.3、创建一个一个带Instrumentation参数的premain()方法package org.example; import java.lang.instrument.Instrumentation; public class Agent { public static void premain(String agentArgs, Instrumentation inst){ System.out.println(agent premain method); inst.addTransformer(new MyTransformer(agentArgs)); } }2.4、创建一个类实现ClassFileTransformer接口的transform(......)方法。byte[] transform(ClassLoader loader, String className, Class? classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)package org.example; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; public class MyTransformer implements ClassFileTransformer { //指定匹配报名(方便测试) private String packageName; MyTransformer(String packageName) { // 从参数中获取指定的包名 System.out.println(Load successfully from args! Package name: packageName); // 将包名保存到成员变量中 this.packageName packageName; } /** * param loader 类加载器 * param className 类名 * param classBeingRedefined 重定义的类 * param protectionDomain 保护域 * param classfileBuffer 类文件字节码 * return 修改后的类字节码返回null则表示不修改 */ Override public byte[] transform(ClassLoader loader, String className, Class? classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { className className.replace(/, .); //这里我们限制下只针对目标报下进行耗时统计 if (!className.startsWith(packageName)) { //System.out.println(Transforming class: className); return classfileBuffer; } //ClassPool:字节码类的缓存池管理所有 CtClass ClassPool classPool ClassPool.getDefault(); try { //将类文件字节码转换成CtClass对象 CtClass ctClass classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); //获取类中所有方法 CtMethod[] declaredMethods ctClass.getDeclaredMethods(); for (CtMethod method : declaredMethods) { //给方法添加一个 long 类型的局部变量 start局部变量需要通过“addLocalVariable”类声明局部变量 method.addLocalVariable(start, CtClass.longType); //在方法体最开头插入赋值语句记录方法开始执行的时间戳 method.insertBefore(start System.currentTimeMillis();); //获取方法全限定名 String methodLongName method.getLongName(); //在方法体末尾插入打印语句用当前时间减去 start 得到耗时 method.insertAfter(System.out.println(\ methodLongName \ \ takes \ (System.currentTimeMillis() - start) \ms\);); } //将CtClass对象转换成字节码 byte[] bytecode ctClass.toBytecode(); return bytecode; } catch (IOException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } return classfileBuffer; } }2.5、修改pom.xml文件pom.xml文件添加maven-assembly-plugin插件。build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId version3.7.1/version configuration !--使用内置的 jar-with-dependencies 描述符打包时会把所有依赖如 javassist的 class 文件一并打入 JAR 生成一个 fat jar。最终产出类似 java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar。 名词fat jar解释:Fat JAR也叫 Uber JAR是指把项目自身的 class 文件和所有依赖的第三方库 class 文件全部打包进一个 JAR 文件中。 -- descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs archive !--自动添加META-INF/MANIFEST.MF -- manifest addClasspathtrue/addClasspath /manifest manifestEntries !--JVM 运行中动态附加的 Agent 入口类通过 Attach API 使用-- Agent-Classorg.example.Agent/Agent-Class !--JVM 启动时加载的 Agent 入口类通过 -javaagent 参数使用-- Premain-Classorg.example.Agent/Premain-Class !--允许 Agent 重新定义类替换整个类的字节码-- Can-Redefine-Classestrue/Can-Redefine-Classes !--允许 Agent 重新转换已加载的类通过 ClassFileTransformer 修改字节码-- Can-Retransform-Classestrue/Can-Retransform-Classes /manifestEntries /archive /configuration executions execution idmake-assembly/id !--打包时执行assembly插件的single目标-- phasepackage/phase goals goalsingle/goal /goals /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.1.0/version /plugin /plugins /build2.6、使用package命令打包成一个代理jar包在项目的target里可以找到一个“Fat jar”。名词fat jar解释: Fat JAR也叫 Uber JAR是指把项目自身的 class 文件和所有依赖的第三方库 class 文件全部打包进一个 JAR 文件中找到这个jar所在文件目录地址使用解压工具解压后找到META-INF/MANIFEST.MF文件使用记事本打开发现内容如下这是默认的文件包含当前的一些版本信息当前工程的启动类它还有别的参数允许你做更多的事情可以用上的有Manifest-Version: 1.0 Created-By: Maven Archiver 3.6.1 Build-Jdk-Spec: 17 Class-Path: javassist-3.30.2-GA.jar Agent-Class: org.example.Agent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: org.example.AgentPremain-Class 包含 premain 方法的类类的全路径名Agent-Class 包含 agentmain 方法的类类的全路径名Class-Path 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符“/”开头则为绝对路径否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的则忽略不表示 JAR 文件的路径。可选Can-Redefine-Classes true表示能重定义此代理所需的类默认值为 false可选Can-Retransform-Classes true 表示能重转换此代理所需的类默认值为 false 可选三、测试javaagent是否生效3.1、新建一个SpringBoot工程3.2、新建一个web方法package org.example.springbootdemo; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController public class DemoController { /** * 模拟耗时操作 * param cost * return */ RequestMapping(/take/{cost}) public String take(PathVariable(name cost) String cost) { try { Thread.sleep(Integer.parseInt(cost) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } return success; } }3.3、使maven的package命令打包这个SpringBoot工程打完包后使用Terminal终端打开该SpringBoot项目的target目录。3.4、指定代理jar运行jar测试命令如下java -javaagent:D:\projects\personal\spring-cloud-alibaba-framework\spring-cloud-alibaba\java-agent\target\java-agent-1.0-SNAPSHOT-jar-with-dependencies.jarorg.example -jar .\springboot-demo-0.0.1-SNAPSHOT.jar命令解释命令解释javajava命令-javaagent:xx/xx.jarjava代理命令xxx/xx.jar表示代理jar所在的目录地址l路径。注意“xx.jarorg.example”则是我们之前定义类MyTransformer初始化时给属性packageName赋值。-jar xxx.jar指定本次要运行的jar包启动后可以看到MyTransformer类构造函数初始化赋值的日志打印。3.5、web访问测试浏览器分别访问http://localhost:8080/task/1http://localhost:8080/task/2http://localhost:8080/task/3查看控制台的耗时统计。