参考文章:
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方法,选择要增强类的进程即可
原文地址:https://www.cnblogs.com/byzgss/p/11745327.html