Java Agent动态监控与热部署

Java Agent热部署

Java Agent是一个依附在目标JVM进程上的一个jar包。一旦JVM跑起来后,通过Java Agent则像一个探针一样插到JVM内部,探到想要的信息,并且可以注入东西进去。即监听与热修改。在修改某个类后,通过Java Agent的instrument机制,将之前的字节码替换为新代码所对应的字节码。

Java Agent的实现和使用

Java Agent最终以jar包形式存在。方法有两种agentmainpremain,主要包含两个部分,一部分是实现代码,一部分是配置文件。配置文件放在META-INF目录下,包括以下配置项:

  1. Manifest-Version: 1.0 //版本
  2. Created-By: sorry //Creator
  3. Agent-Class: agent.MyCustomAgent //agentmain方法所在类
  4. Can-Redefine-Classes: true //是否可实现类的重定义
  5. Can-Retransform-Classes: true //是否可实现字节码替换
  6. Premain-Class: agent.MyCustomAgent //premain方法所在类

在方法中插入代码主要用到字节码修改技术,字节码修改技术主要有javassist、ASM,以及ASM的高级封装可扩展性cglib,这里使用javassist。可以引入maven包。

  1. <dependency>
  2. <groupId>javassist</groupId>
  3. <artifactId>javassist</artifactId>
  4. <version>3.11.0.GA</version>
  5. </dependency>

Java Agent有两种启动方式,一种直接以JVM启动参数-javaagent:xxx.jar的形式随JVM一起启动,这种情况下,会调用premain方法,并且是主进程的main方法之外执行。另一种则是以loadAgent方法动态attach到目标JVM上,这种情况下,会执行agentmain方法。
代码实现如下:

  1. package agent;
  2. import java.lang.instrument.Instrumentation;
  3. public class MyCustomAgent {
  4. /**
  5. * jvm 参数形式启动,运行此方法
  6. * @param agentArgs
  7. * @param inst
  8. */
  9. public static void premain(String agentArgs, Instrumentation inst){
  10. System.out.println("premain");
  11. customLogic(inst);
  12. }
  13. /**
  14. * 动态 attach 方式启动,运行此方法
  15. * @param agentArgs
  16. * @param inst
  17. */
  18. public static void agentmain(String agentArgs, Instrumentation inst){
  19. System.out.println("agentmain");
  20. customLogic(inst);
  21. }
  22. /**
  23. * 打印所有已加载的类名称
  24. * 修改字节码
  25. * @param inst
  26. */
  27. private static void customLogic(Instrumentation inst){
  28. inst.addTransformer(new MyTransformer(), true);
  29. Class[] classes = inst.getAllLoadedClasses();
  30. for(Class cls :classes){
  31. System.out.println(cls.getName());
  32. }
  33. }
  34. }

参数有agentArgs和inst,其中agentArgs是启动Java Agent时进来的参数,比如-javaagent:xxx.jar agentArgs。Instrumentation Java开放出来的专门用于字节码修改和程序监控的实现。我们要实现的修改字节码就是基于它实现。其中inst.getAllLoadedClassed()一个方法就实现了获取所有已加载类的功能。
inst.addTransformer方法则是实现字节码修改的关键,后面的参数就是实现字节码修改的实现类,代码如下:

  1. package agent;
  2. import javassist.ClassPool;
  3. import javassist.CtClass;
  4. import javassist.CtMethod;
  5. import java.io.ByteArrayInputStream;
  6. import java.lang.instrument.ClassFileTransformer;
  7. import java.lang.instrument.IllegalClassFormatException;
  8. import java.security.ProtectionDomain;
  9. public class MyTransformer implements ClassFileTransformer {
  10. @Override
  11. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  12. System.out.println("正在加载类:"+ className);
  13. if (!"attach/Person".equals(className)){
  14. return classfileBuffer;
  15. }
  16. CtClass cl = null;
  17. try {
  18. ClassPool classPool = ClassPool.getDefault();
  19. cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
  20. CtMethod ctMethod = cl.getDeclaredMethod("test");
  21. System.out.println("获取方法名称:"+ ctMethod.getName());
  22. ctMethod.insertBefore("System.out.println(\" 动态插入的打印语句 \");");
  23. ctMethod.insertAfter("System.out.println($_);");
  24. byte[] transformed = cl.toBytecode();
  25. return transformed;
  26. }catch (Exception e){
  27. e.printStackTrace();
  28. }
  29. return classfileBuffer;
  30. }
  31. }

以上的逻辑是当碰到加载类是attach.Person时,在其中的test方法开始时插入一条打印语句,打印内容是“动态插入的打印语句”,在test方法结尾处,打印返回值,其中$_就是返回值,这是javassist里特定的标识符。在pom.xml中加入以下配置:

  1. <build>
  2. <plugins>
  3. <plugin>
  4. <groupId>org.apache.maven.plugins</groupId>
  5. <artifactId>maven-assembly-plugin</artifactId>
  6. <configuration>
  7. <archive>
  8. <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
  9. </archive>
  10. <descriptorRefs>
  11. <descriptorRef>jar-with-dependencies</descriptorRef>
  12. </descriptorRefs>
  13. </configuration>
  14. </plugin>
  15. <plugin>
  16. <groupId>org.apache.maven.plugins</groupId>
  17. <artifactId>maven-compiler-plugin</artifactId>
  18. <configuration>
  19. <source>6</source>
  20. <target>6</target>
  21. </configuration>
  22. </plugin>
  23. </plugins>
  24. </build>

用maven的maven-assembly-pubgin插件,注意manifestFile指定MANIFEST.MF所在路径,然后指定jar-with-dependencies,将依赖包打进去。
运行打包命令:

  1. mvn assembly:assembly

写一个简单的测试项目,用来作为目标JVM,稍后以两种方式将Java Agent挂到这个测试项目上。

  1. package attach;
  2. import java.util.Scanner;
  3. public class RunJvm {
  4. public static void main(String[] args){
  5. System.out.println("按数字键1调用测试方法");
  6. while(true){
  7. Scanner reader = new Scanner(System.in);
  8. int number = reader.nextInt();
  9. if(number==1){
  10. Person person = new Person();
  11. person.test();
  12. }
  13. }
  14. }
  15. }

一个简单的main方法,用while方式保证线程不退出,且当输入数字1后调用person.test()方法。

  1. package attach;
  2. public class Person {
  3. public String test(){
  4. System.out.println("执行测试方法");
  5. return "I'm ok";
  6. }
  7. }

以命令行的方式运行(premain)

  1. java -javaagent:reAgent.jar -jar reAttach.jar

以动态attach的方式运行
测试之前要先运行reAttach.jar,在利用jps -l查找此进程id。
动态attach需要代码实现,实现代码如下:

  1. import com.sun.tools.attach.VirtualMachine;
  2. public class AttachAgent {
  3. public static void main(String[] args) throws Exception{
  4. VirtualMachine vm = VirtualMachine.attach("8196");
  5. vm.loadAgent("F:\\reAgent.jar");
  6. //vm.loadAgent("E:\\reAgent\\target\\sorry-1.0-SNAPSHOT-jar-with-dependencies.jar");
  7. }
  8. }

运行上面的main方法,并在测试程序中输入”1”也会得到结果。
结果:
Java Agent - 图1