学习并模拟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 类中(截图是jdk17):
Instrumentation 类 定义如下:
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 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程,一个工程是用来承载 javaagent类,单独的打成jar包;一个工程是javaagent需要去代理的类。即javaagent会在这个工程中的main方法启动之前去做一些事情。
2.1、首先来实现javaagent工程
工程目录结构如下:
2.2、添加javassist依赖包
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.30.2-GA</version> </dependency>2.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> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.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> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <!--自动添加META-INF/MANIFEST.MF --> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <!--JVM 运行中动态附加的 Agent 入口类(通过 Attach API 使用)--> <Agent-Class>org.example.Agent</Agent-Class> <!--JVM 启动时加载的 Agent 入口类(通过 -javaagent 参数使用)--> <Premain-Class>org.example.Agent</Premain-Class> <!--允许 Agent 重新定义类(替换整个类的字节码)--> <Can-Redefine-Classes>true</Can-Redefine-Classes> <!--允许 Agent 重新转换已加载的类(通过 ClassFileTransformer 修改字节码)--> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <!--打包时执行assembly插件的single目标--> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> </plugin> </plugins> </build>2.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.jar=org.example -jar .\springboot-demo-0.0.1-SNAPSHOT.jar
命令解释:
| 命令 | 解释 |
| java | java命令 |
| -javaagent:xx/xx.jar | java代理命令,xxx/xx.jar表示代理jar所在的目录地址l路径。 注意:“xx.jar=org.example”,则是我们之前定义类MyTransformer初始化时给属性packageName赋值。 |
| -jar xxx.jar | 指定本次要运行的jar包 |
启动后,可以看到MyTransformer类构造函数初始化赋值的日志打印。
3.5、web访问测试
浏览器分别访问http://localhost:8080/task/1,http://localhost:8080/task/2,http://localhost:8080/task/3,查看控制台的耗时统计。
