javaAgent和Java字节码增强技术的学习与实践
参考文章:
https://www.cnblogs.com/chiangchou/p/javassist.html
https://blog.csdn.net/u010039929/article/details/62881743
https://www.jianshu.com/p/0f64779cdcea
【本文代码下载】
【背景】
最近在工作中进行程序的性能调优时,想起之前同事的介绍的阿里的Java在线诊断工具 —— arthas,决定试用一下。
这玩意,是真的好用,能在对被检测程序 不做 任何改动 和 设置 的情况下,无侵入的对运行中的程序进行性能分析诊断,监控进入指定方法的请求并展示请求的参数,甚至在线热更新代码,
通过查阅资料发现,arthas是基于javaAgent技术 和 Java字节码增强技术 实现的,所以接下来就开始介绍javaAgent 和 Java字节码 技术的学习及案例
【知识准备】
什么是java agent?
Java agent是在JDK1.5引入的,是一种可以动态修改Java字节码的技术。java类编译之后形成字节码被JVM执行,JVM在执行这些字节码之前获取这些字节码信息,并且对这些字节码进行修改,来完成一些额外的功能,这种就是java agent技术。
我们可以使用agent技术构建一个独立于应用程序的代理程序(即为Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能,并且比起 Spring的 AOP 更加的优美。
Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供),待会都会讲解
什么是字节码增强技术?
个人理解,是在Java字节码生成之后,运行期对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。
通常可以用 ASM 或 javassist 框架来修改字节码
JVM Attach机制
jvm attach机制上JVM提供的一种JVM进程间通信的功能,能让一个进程传命令给另一个进程,并进行一些内部的操作,比如进行线程dump,那么就需要执行jstack进行,然后把pid等参数传递给需要dump的线程来执行,这就是一种java attach。
Class Transform的实现
第一次类加载的时候要求被transform的场景,在加载类文件的时候发出ClassFileLoad事件,交给instrument agent来调用java agent里注册的ClassFileTransformer实现字节码的修改
【案例】
在了解java agent 和 字节码增强技术 后,我们可以结合起来做一个小案例:
在指定类的指定方法执行前,先打印一串11111111
正如上文所述,Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent,接下来两种案例都会给出,本次案例使用javassist 框架
=======================准备好被增强类的代码=======================
被增强类的pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>java_agent</artifactId> 7 <groupId>xcy</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>app</artifactId> 13 14 <dependencies> 15 <!-- 在被增强类的pom文中,需要加入jdk的tools工具,或者自己把该jar包放入到项目中引用,因为我发现在默认情况下,idea即便引用jdk,用的包居然都是jdk中的jre,jre中没有tools.jar这个包,就会导致被增强类启动后,没有attach服务,无法连接过来 --> 16 <dependency> 17 <groupId>jdk.tools</groupId> 18 <artifactId>jdk.tools</artifactId> 19 <version>jdk1.8.0_121</version> 20 <scope>system</scope> 21 <systemPath>F:/ProgramFiles/java/jdk1.8.0_121/lib/tools.jar</systemPath> 22 </dependency> 23 </dependencies> 24 25 <build> 26 <!-- <finalName>App</finalName>--> 27 <plugins> 28 <plugin> 29 <groupId>org.apache.maven.plugins</groupId> 30 <artifactId>maven-compiler-plugin</artifactId> 31 <configuration> 32 <source>1.8</source> 33 <target>1.8</target> 34 <encoding>utf-8</encoding> 35 </configuration> 36 </plugin> 37 <plugin> 38 <groupId>org.apache.maven.plugins</groupId> 39 <artifactId>maven-shade-plugin</artifactId> 40 <version>1.2.1</version> 41 <executions> 42 <execution> 43 <!-- <phase>package</phase>--> 44 <goals> 45 <goal>shade</goal> 46 </goals> 47 <configuration> 48 <transformers> 49 <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 50 <mainClass>com.app.App</mainClass> 51 </transformer> 52 </transformers> 53 </configuration> 54 </execution> 55 </executions> 56 </plugin> 57 </plugins> 58 </build> 59 </project>
被增强的类
备注:代码在java_agent项目中的app模块下
1 package com.app;
2
3 public class App {
4 public static void main(String[] args) {
5 hello();
6 }
7
8 public static void hello() {
9 System.out.println("hello");
10 }
11 }
=======================主程序之前运行的Agent方式=======================
备注:代码在java_agent项目中的agent_non_attach模块下
pom.xml
agent通过MANIFEST.MF 中指定的Premain-Class参数,获取代理程序入口,所以写好的代理类想要运行,在打 jar 包前,还需要要在 MANIFEST.MF 中指定代理程序入口。
在pom文件中,配置好Premain-Class,就会封装到META-INF下的MANIFEST.MF中
备注:与 agent 相关的参数
- Premain-Class :JVM 启动时指定了代理,此属性指定代理类,即包含 premain 方法的类。
- Agent-Class :JVM动态加载代理,此属性指定代理类,即包含 agentmain 方法的类。
- Boot-Class-Path :设置引导类加载器搜索的路径列表,列表中的路径由一个或多个空格分开。
- Can-Redefine-Classes :布尔值(true 或 false)。是否能重定义此代理所需的类。
- Can-Retransform-Classes :布尔值(true 或 false)。是否能重转换此代理所需的类。
- Can-Set-Native-Method-Prefix :布尔值(true 或 false)。是否能设置此代理所需的本机方法前缀。
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>java_agent</artifactId> 7 <groupId>xcy</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>agent_non_attach</artifactId> 13 14 <dependencies> 15 <!-- https://mvnrepository.com/artifact/org.javassist/javassist --> 16 <dependency> 17 <groupId>org.javassist</groupId> 18 <artifactId>javassist</artifactId> 19 <version>3.26.0-GA</version> 20 </dependency> 21 </dependencies> 22 23 <build> 24 <plugins> 25 <plugin> 26 <groupId>org.apache.maven.plugins</groupId> 27 <artifactId>maven-compiler-plugin</artifactId> 28 <configuration> 29 <source>1.8</source> 30 <target>1.8</target> 31 <encoding>utf-8</encoding> 32 </configuration> 33 </plugin> 34 <plugin> 35 <groupId>org.apache.maven.plugins</groupId> 36 <artifactId>maven-shade-plugin</artifactId> 37 <version>3.0.0</version> 38 <executions> 39 <execution> 40 <phase>package</phase> 41 <goals> 42 <goal>shade</goal> 43 </goals> 44 <configuration> 45 <transformers> 46 <transformer 47 implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 48 <manifestEntries> 49 <Premain-Class>com.agent.non.attach.demo.Agent</Premain-Class> 50 </manifestEntries> 51 </transformer> 52 </transformers> 53 </configuration> 54 </execution> 55 </executions> 56 </plugin> 57 </plugins> 58 </build> 59 </project>
Agent代理类
addTransformer:注册一个Transformer,从此之后的类加载都会被 transformer 拦截。
VM启动后动态加载的 agent,Instrumentation 会通过 agentmain 方法传入代理程序,agentmain 在 main 函数开始运行后才被调用。
对于VM启动时加载的 agent,Instrumentation 会通过 premain 方法传入代理程序,premain 方法会在程序 main 方法执行之前被调用。此时大部分Java类都没有被加载(“大部分”是因为,agent类本身和它依赖的类还是无法避免的会先加载的),是一个对类加载埋点做手脚(addTransformer)的好机会。但这种方式有很大的局限性,Instrumentation 仅限于 main 函数执行前,此时有很多类还没有被加载,如果想为其注入 Instrumentation 就无法办到。
1 package com.agent.non.attach.demo;
2
3 import java.lang.instrument.Instrumentation;
4
5 public class Agent {
6 /**
7 * agentArgs 是 premain 函数得到的程序参数,通过 -javaagent 传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
8 * inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
9 */
10 public static void premain(String agentOps, Instrumentation inst) {
11 System.out.println("=========premain方法执行========");
12 // 添加Transformer
13 inst.addTransformer(new MyTransformer("com.app.App", "hello"));
14 }
15
16 /**
17 * 带有 Instrumentation 参数的 premain 优先级高于不带此参数的 premain。
18 * 如果存在带 Instrumentation 参数的 premain,不带此参数的 premain 将被忽略。
19 * @param agentArgs
20 */
21 public static void premain(String agentArgs) {
22
23 }
24 }
MyTransformer类
拦截指定方法,进行类增强
1 package com.agent.non.attach.demo;
2
3 import javassist.ClassPool;
4 import javassist.CtClass;
5 import javassist.CtMethod;
6
7 import java.lang.instrument.ClassFileTransformer;
8 import java.lang.instrument.IllegalClassFormatException;
9 import java.security.ProtectionDomain;
10
11 public class MyTransformer implements ClassFileTransformer {
12 private String targetClassName;//被增强类的类名
13 private String targetMethodName;//被增强的方法名
14
15 public MyTransformer(String targetClassName, String targetMethodName) {
16 this.targetClassName = targetClassName;
17 this.targetMethodName = targetMethodName;
18 }
19
20 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
21 className = className.replace("/", ".");
22 if (className.equals(targetClassName)) {
23 CtClass ctclass = null;
24 try {
25 ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
26 CtMethod ctmethod = ctclass.getDeclaredMethod(targetMethodName);// 得到这方法实例
27 ctmethod.insertBefore("System.out.println(1111111);");
28 return ctclass.toBytecode();
29 } catch (Exception e) {
30 System.out.println(e.getMessage());
31 e.printStackTrace();
32 }
33 }
34 return null;
35 }
36 }
运行
运行有两种方式:
1、直接运行,格式:java -javaagent:agent的jar路径 被增强jar的路径
例如:java -jar -javaagent:agent_non_attach-1.0-SNAPSHOT.jar app-1.0-SNAPSHOT.jar
2、在idea中运行
在被增强类项目中新建一个启动类,在该类中添加main方法,在运行时,指定VM options参数:-javaagent: agent的jar包路径 被增强的jar包路径
例如:-javaagent:F:\workspace\java_agent\agent_non_attach\target\agent_non_attach-1.0-SNAPSHOT.jar
=======================主程序之后运行的Agent方式=======================
备注:代码在java_agent项目中的agent_attach模块下
被增强类的pom.xml
在被增强类的pom文中,需要加入jdk的tools工具,或者自己把该jar包放入到项目中引用,因为我发现在默认情况下,idea即便引用jdk,用的包居然都是jdk中的jre,jre中没有tools.jar这个包,就会导致被增强类启动后,没有attach服务,无法连接过来
1 <dependency> 2 <groupId>jdk.tools</groupId> 3 <artifactId>jdk.tools</artifactId> 4 <version>jdk1.8.0_121</version> 5 <scope>system</scope> 6 <systemPath>F:/ProgramFiles/java/jdk1.8.0_121/lib/tools.jar</systemPath> 7 </dependency>
KeepRunMain类
该agent需要在被增强类运行的时候执行,所以需要让被增强类一直运行
1 package com.app;
2
3 public class KeepRunMain {
4 public static void main(String[] args) throws InterruptedException {
5 String[] params = {};
6 while (true) {
7 App.hello();
8 Thread.sleep(1000L);
9 }
10 }
11 }
agent的pom.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 5 <parent> 6 <artifactId>java_agent</artifactId> 7 <groupId>xcy</groupId> 8 <version>1.0-SNAPSHOT</version> 9 </parent> 10 <modelVersion>4.0.0</modelVersion> 11 12 <artifactId>agent_attach</artifactId> 13 14 <dependencies> 15 <!-- https://mvnrepository.com/artifact/org.javassist/javassist --> 16 <dependency> 17 <groupId>org.javassist</groupId> 18 <artifactId>javassist</artifactId> 19 <version>3.26.0-GA</version> 20 </dependency> 21 22 <dependency> 23 <groupId>jdk.tools</groupId> 24 <artifactId>jdk.tools</artifactId> 25 <version>jdk1.8.0_121</version> 26 <scope>system</scope> 27 <systemPath>F:/ProgramFiles/java/jdk1.8.0_121/lib/tools.jar</systemPath> 28 </dependency> 29 </dependencies> 30 31 <build> 32 <plugins> 33 <plugin> 34 <groupId>org.apache.maven.plugins</groupId> 35 <artifactId>maven-compiler-plugin</artifactId> 36 <configuration> 37 <source>1.8</source> 38 <target>1.8</target> 39 <encoding>utf-8</encoding> 40 </configuration> 41 </plugin> 42 <plugin> 43 <groupId>org.apache.maven.plugins</groupId> 44 <artifactId>maven-shade-plugin</artifactId> 45 <version>3.0.0</version> 46 <executions> 47 <execution> 48 <phase>package</phase> 49 <goals> 50 <goal>shade</goal> 51 </goals> 52 <configuration> 53 <transformers> 54 <transformer 55 implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 56 <manifestEntries> 57 <Agent-Class>com.agent.attach.demo.Agent</Agent-Class> 58 <Can-Retransform-Classes>true</Can-Retransform-Classes> 59 </manifestEntries> 60 </transformer> 61 </transformers> 62 </configuration> 63 </execution> 64 </executions> 65 </plugin> 66 </plugins> 67 </build> 68 </project>
Agent类
1 package com.agent.attach.demo;
2
3 import java.lang.instrument.Instrumentation;
4 import java.lang.instrument.UnmodifiableClassException;
5
6 public class Agent {
7 public static String className="com.app.App";
8 public static String methon="hello";
9 static {
10 System.out.println("ddd");
11 }
12 public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
13 Class[] allClass = inst.getAllLoadedClasses();
14 for (Class c : allClass) {
15 System.out.println(c.getName());
16 if(c.getName().equals(className)){
17 System.out.println("agent loaded");
18 inst.addTransformer(new MyTransformer(className, methon), true);
19 inst.retransformClasses(c);
20 }
21 }
22 }
23 }
MyTransformer类
1 package com.agent.attach.demo;
2
3 import javassist.ClassPool;
4 import javassist.CtClass;
5 import javassist.CtMethod;
6
7 import java.lang.instrument.ClassFileTransformer;
8 import java.lang.instrument.IllegalClassFormatException;
9 import java.security.ProtectionDomain;
10
11 public class MyTransformer implements ClassFileTransformer {
12 private String targetClassName;//被增强类的类名
13 private String targetMethodName;//被增强的方法名
14
15 public MyTransformer(String targetClassName, String targetMethodName) {
16 this.targetClassName = targetClassName;
17 this.targetMethodName = targetMethodName;
18 }
19
20 @Override
21 public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
22 try {
23 CtClass ctClass = ClassPool.getDefault().get(this.targetClassName);
24 CtMethod ctMethod=ctClass.getDeclaredMethod(this.targetMethodName);
25 System.out.println(ctMethod.getName());
26 ctMethod.insertBefore("System.out.println(\" 11111111111111111111111\");");
27 ctClass.writeFile();
28 return ctClass.toBytecode();
29 } catch (Exception e) {
30 System.out.println(e.getMessage());
31 }
32 return null;
33 }
34 }
AttachMain类
1 package com.agent.attach.demo;
2
3 import com.sun.tools.attach.AttachNotSupportedException;
4 import com.sun.tools.attach.VirtualMachine;
5 import com.sun.tools.attach.VirtualMachineDescriptor;
6
7 import java.util.List;
8
9 public class AttachMain {
10 public static void main(String args[]) throws AttachNotSupportedException {
11 VirtualMachine vm;
12 List<VirtualMachineDescriptor> vmList= VirtualMachine.list();
13 if(vmList==null || vmList.isEmpty()){
14 System.out.println("当前没有java程序运行");
15 return;
16 }
17
18 //展示所有运行中的java程序
19 System.out.println("当前运行中的java程序:");
20 for(int i=0;i<vmList.size();i++){
21 System.out.println("["+i+"] "+vmList.get(i).displayName()+" ,id:"+vmList.get(i).id()+" ,provider:"+vmList.get(i).provider());
22 }
23 System.out.println("请选择(输入序号):");
24
25 //选择其中一个java进程进行增强
26 try{
27 int num=System.in.read()-48;
28 if(num!=-1&&num<vmList.size()){
29 vm= VirtualMachine.attach(vmList.get(num));
30 vm.loadAgent("F:\\workspace\\java_agent\\agent_attach\\target\\agent_attach-1.0-SNAPSHOT.jar");
31 System.in.read();
32 }
33 }catch(Exception e){
34 e.printStackTrace();
35 }
36 }
37 }
运行方式
直接执行AttachMain类的main方法,选择要增强类的进程即可