Java Agent技术

打印 上一主题 下一主题

主题 848|帖子 848|积分 2546

在定位公司问题的时候,需要了解一下skywalking的相关知识,而agent就提上了日程。
官网文档
Agent技术是Jdk在1.5版本之后,所提供的一个在jvm启动前后对部分java类代理加强的机制。由于是直接修改字节码,并不会对业务代码有注入,所以可以很好的应用于监控或者热部署等场景。
正常所提到的Agent一般都是部署成jar包的样子,比如agent-1.0-SNAPSHOT.jar。
在这个jar包中,要添加一个MANIFEST.MF文件,在文件中指定jar包的代理类,比如下面代码中的Premain-Class。
在对应的代理类,要实现一个permain方法或者agentmain方法,这样jvm可以通过MANIFEST找到类,通过类再找到对应的方法,从而进行加强,所以加强逻辑是在permain方法或者agentmain方法内部实现的。
  1. Manifest-Version: 1.0
  2. Built-By: qisi
  3. Premain-Class: com.qisi.agent.InterviewAgent
  4. Agent-Class: com.qisi.agent.InterviewAgent
  5. Can-Redefine-Classes: true
  6. Can-Retransform-Classes: true
  7. Class-Path: byte-buddy-1.10.22.jar
  8. Created-By: Apache Maven 3.8.1
  9. Build-Jdk: 1.8.0_332
复制代码
  1. public class InterviewAgent {
  2.     public static void premain(String agentArgs, Instrumentation instrumentation) {
  3.     }
  4.     public static void agentmain(String agentArgs, Instrumentation instrumentation) {
  5.     }
  6. }
复制代码
而如果在permain或者agentmain方法打上debug可以发现,执行时是通过sun.instrument.InstrumentationImpl#loadClassAndCallPremain和sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain两个方法通过反射来执行到我们指定的类的。
Agent技术有两种场景,一种是在jvm启动之前,通过-javaagent:path来指定jar包,像是skywalking就是采用的这种方式;另一种则是在jvm启动之后,通过attach指定的进程,对jvm中的类进行加强,arthas就是采用的这种方式。
在具体介绍这两种方式之前,需要先讲一下Instrumentation相关类和接口
java.lang.Instrumentation

Instrumentation

Instrumentation相关的类都在java.lang.Instrumentation包下,两个异常,两个接口,一个类。

两个异常在这里不做介绍,功能就像类名一样。核心的其实是Instrumentation接口,本文仅关注红框内的几个方法。这几个方法都是通过permain和agentmain获取到的instrumentation实例进行的操作。

从时间发展来看,其中jdk1.5开始支持的是下面几个方法,也就是说在jdk5的时候,仅支持添加和移除类转换器,且添加的类转换器只能在加载和重定义的时候使用。就是说如果类没有加载,那么通过addTransformer方法注册的ClassFileTransformer就可以对这个类进行增强,否则一旦类已经加载完毕,则只能通过redefineClasses,完全替换类定义再次触发loadClass来增强
  1. addTransformer(ClassFileTransformer transformer)
  2. removeTransformer(ClassFileTransformer transformer)
  3. isRedefineClassesSupported();//依赖于MANIFEST中的Can-Redefine-Classes值
  4. redefineClasses(ClassDefinition... definitions)
复制代码
而从jdk1.6开始,增加了一个retransformClasses的概念。retransform和redefine的区别,前者是在原有类的基础上进行修改,后者则是完全重定义,不使用原有类做任何参考。
需要注意的事,只有在首次调用addTransformer时,将canRetransform设置为true的类,才可以被重新转换。
  1. addTransformer(ClassFileTransformer transformer, boolean canRetransform);
  2. isRetransformClassesSupported();
  3. retransformClasses(Class<?>... classes)//依赖于MANIFEST中的Can-Retransform-Classes值
  4. isModifiableClass(Class<?> theClass);
复制代码
ClassFileTransformer、ClassDefinition

这两个类其实都是Instrumentation接口方法的入参,其中用的比较多的应该是ClassFileTransformer。这个类只有一个transform,jvm类加载的时候都会调用一遍这个方法。如果需要加强,那么就利用给定的参数,进行字节码的改动,将改动后的字节码作为返回值返回;如果无需增强,则直接返回null即可。
  1. byte[]
  2. transform(  ClassLoader         loader,
  3.             String              className,
  4.             Class<?>            classBeingRedefined,
  5.             ProtectionDomain    protectionDomain,
  6.             byte[]              classfileBuffer)
复制代码
ClassDefinition也类似,不过是在对象里重新绑定class和byte的关系
  1. public final class ClassDefinition {
  2.     /**
  3.      *  The class to redefine
  4.      */
  5.     private final Class<?> mClass;
  6.     /**
  7.      *  The replacement class file bytes
  8.      */
  9.     private final byte[]   mClassFile;
复制代码
实践

MANIFEST.MF配置

在pom文件中添加下面的代码,根据需要修改参数值
  1. <build>
  2.     <plugins>
  3.         <plugin>
  4.             <groupId>org.apache.maven.plugins</groupId>
  5.             <artifactId>maven-jar-plugin</artifactId>
  6.             <configuration>
  7.                 <archive>
  8.                     <addMavenDescriptor>false</addMavenDescriptor>
  9.                     <manifest>
  10.                         <addClasspath>true</addClasspath>
  11.                     </manifest>
  12.                     <manifestEntries>
  13.                         <Premain-Class>
  14.                             com.qisi.agent.InterviewByteButtyAgent
  15.                         </Premain-Class>
  16.                         <Agent-Class>
  17.                             com.qisi.agent.InterviewByteButtyAgent
  18.                         </Agent-Class>
  19.                         <Can-Redefine-Classes>
  20.                             true
  21.                         </Can-Redefine-Classes>
  22.                         <Can-Retransform-Classes>
  23.                             true
  24.                         </Can-Retransform-Classes>
  25.                         <Built-By>
  26.                             qisi
  27.                         </Built-By>
  28.                     </manifestEntries>
  29.                 </archive>
  30.             </configuration>
  31.         </plugin>
  32.     </plugins>
  33. </build>
复制代码
-javaagent:

在这种方式下,起作用的是permain,也就是说-javaagent和permain方法是配套使用的。
核心就是添加一个自定义的ClassFileTransformer,可以另起一个类,也可以这样匿名类。
如果只是熟悉流程可以像下面一样,直接打印一些日志,不去修改类;
  1.     public static void premain(String agentArgs, Instrumentation instrumentation) {
  2.         System.out.println("enhance by premain,params:"+agentArgs);
  3.         instrumentation.addTransformer(new ClassFileTransformer() {
  4.             @Override
  5.             public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  6.                                     ProtectionDomain protectionDomain, byte[] classfileBuffer)
  7.                     throws IllegalClassFormatException {
  8.                 System.out.println("premain load Class     :" + className);
  9.                 return classfileBuffer;
  10.             }
  11.         }, true);
  12.     }
复制代码
如果要真实修改,需要引入asm javassist bytebuddy等修改字节码的框架。下面这部分就是使用了bytebuddy,作用是让任何类的testAgent方法,都返回固定值transformed
  1. public static void premain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
  2.     System.out.println("enhance by permain InterviewByteButtyAgent,params:"+agentArgs);
  3.     new AgentBuilder.Default().type(any()).transform(new AgentBuilder.Transformer() {
  4.         @Override
  5.         public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
  6.             return builder.method(named("testAgent"))
  7.                     .intercept(FixedValue.value("transformed"));
  8.         }
  9.     }).installOn(instrumentation);
  10. }
复制代码
编写完之后,就可以在任意项目添加一个存在testAgent方法的进行尝试了,比如
java -javaagent:/xxxx/path/agent-1.0-SNAPSHOT.jar=key1:value1,key2:value2 -jar AppDemo.jar
attach

agentmain

这种方式需要实现agentmain方法,和permian不太一样的地方是需要在addTransformer之后触发需要retransformClasses想要加强的类。
  1. public static void agentmain(String agentArgs, Instrumentation instrumentation) {
  2.     System.out.println("enhance by agentmain,params:"+agentArgs);
  3.     instrumentation.addTransformer(new ClassFileTransformer() {
  4.         @Override
  5.         public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  6.                                 ProtectionDomain protectionDomain, byte[] classfileBuffer)
  7.                 throws IllegalClassFormatException {
  8.             System.out.println("agentmain load Class     :" + className);
  9.             return classfileBuffer;
  10.         }
  11.     }, true);
  12.     try {
  13.         instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
  14.     } catch (UnmodifiableClassException e) {
  15.         e.printStackTrace();
  16.     } catch (ClassNotFoundException e) {
  17.         e.printStackTrace();
  18.     }
  19. }
复制代码
同样,提供一个bytebuddy的例子,下面这个则是指定修改FirstRequestController的testAgent方法的返回值为transformed
  1. public static void agentmain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
  2.     System.out.println("enhance by agentmain InterviewByteButtyAgent,params:"+agentArgs);
  3.     //这里RedefinitionStrategy必须注意,默认的DISABLED是不支持retransform
  4.     new AgentBuilder.Default().with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).type(new AgentBuilder.RawMatcher() {
  5.         @Override
  6.         public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, Class<?> classBeingRedefined, ProtectionDomain protectionDomain) {
  7.             return typeDescription.getName().contains("FirstRequestController");
  8.         }
  9.     }).transform(new AgentBuilder.Transformer() {
  10.         @Override
  11.         public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
  12.             System.out.println("enhance"+typeDescription.getName());
  13.             return builder.method(named("testAgent"))
  14.                     .intercept(FixedValue.value("transformed"));
  15.         }
  16.         //这里采用disableClassFormatChanges的方案,好像还可以使用advice
  17.     }).disableClassFormatChanges().installOn(instrumentation);
  18.     try {
  19.         instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
  20.     } catch (UnmodifiableClassException e) {
  21.         e.printStackTrace();
  22.     } catch (ClassNotFoundException e) {
  23.         e.printStackTrace();
  24.     }
  25. }
复制代码
VirtualMachine

不同于-javaagent命令,这里需要使用自jdk6开始提供的VirtualMachine类,在tool.jar包里
下面的方法是我参考arthas写的一个attach的流程,选择我们想要attach的进程,然后加载我们上面写好的jar包就好了。
  1. public class AgentTest {
  2.     public static void main(String[] args) throws IOException, AttachNotSupportedException {
  3.         String pid = null;
  4.         try {
  5.             Process jps = Runtime.getRuntime().exec("jps");
  6.             InputStream inputStream = jps.getInputStream();
  7.             BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
  8.             String line;
  9.             while ((line = bufferedReader.readLine()) != null) {
  10.                 System.out.println(line);
  11.             }
  12.             System.out.println("选择要attach的进程");
  13.             pid= new Scanner(System.in).nextLine();
  14.             System.out.println("选择的pid是"+pid);
  15.         } catch (IOException e) {
  16.             e.printStackTrace();
  17.         }
  18.         for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
  19.             if (virtualMachineDescriptor.id().equals(pid)){
  20.                 VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor);
  21.                 try {
  22.                     attach.loadAgent("/xxxxx/agent/target/agent-1.0-SNAPSHOT.jar","参数1,参数2");
  23.                 } catch (AgentLoadException e) {
  24.                     e.printStackTrace();
  25.                 } catch (AgentInitializationException e) {
  26.                     e.printStackTrace();
  27.                 } finally {
  28.                     attach.detach();
  29.                 }
  30.                 break;
  31.             }
  32.         }
  33.     }
  34. }
复制代码
参考文档:

探秘 Java  热部署二(Java agent premain)
JAVA热更新1:Agent方式热更 | 花隐间-JAVA游戏技术解决方案
ByteBuddy入门教程

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

祗疼妳一个

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表