javaAgent和Java字节码增强技术的学习与实践

Posted 白羊座怪蜀黍

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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方法,选择要增强类的进程即可

以上是关于javaAgent和Java字节码增强技术的学习与实践的主要内容,如果未能解决你的问题,请参考以下文章

字节码增强技术之 Java Agent 入门

记一次多个Java Agent同时使用的类增强冲突问题及分析

字节码JavaAgent的全链路监控篇二,通过字节码增加监控执行 耗时

字节码JavaAgent ByteBuddy操作监控方法 字节码

Java逆向基础之初识javaagent

字节码基于JavaAgent的全链路监控四-JVM内存与GC信息