• Java动态编程之Instrumentation


    需求

    在不修改代码重新发布的情况下,查看一个线上正运行代码的方法的入参、返回值,或者增加某些日志的打印

    解决方案

    arthas
    btrace
    自定义classloader,发现需要动态加载的类定义变更时对类进行重新加载打破双亲委派
    本文暂不讨论自定义classloader方式,工具的具体使用方法可以查看官方文档,那么这些工具的底层是如何实现的呢?没错他们底层都使用了JDK提供的Instrumentation接口。那么Instrumentation接口如何使用呢?

    用法

    以下内容为翻译的该接口的javadoc 该类提供测试Java编程语言代码所需的服务。Instrumentation是将字节码添加至方法中为收集工具所使用的数据。因为改变是纯添加动作,这些工具不会改变应用状态或行为。像这样良性的工具案例包含监控代理agents,profilers,coverage analyzers, and event loggers. 获取该接口实例的方法有两种:

    当JVM通过显示的指定一个代理类的方式运行。在这种情形下,Instrumentation实例通过代理类的premain方法传入
    JVM提供一个机制在JVM运行之后启动代理。在这种情形下,Instrumentation实例通过代理代码的agentmain方法传入

    显示指定代理类方式获取

    案例代码取自arthas pom配置为生成的manifest文件中指定premain代码提供类以及代理类提供类

    1. <plugin>
    2. <groupId>org.apache.maven.pluginsgroupId>
    3. <artifactId>maven-assembly-pluginartifactId>
    4. <executions>
    5. <execution>
    6. <goals>
    7. <goal>singlegoal>
    8. goals>
    9. <phase>packagephase>
    10. <configuration>
    11. <descriptorRefs>
    12. <descriptorRef>jar-with-dependenciesdescriptorRef>
    13. descriptorRefs>
    14. <archive>
    15. <manifestEntries>
    16. <Premain-Class>com.taobao.arthas.agent.AgentBootstrapPremain-Class>
    17. <Agent-Class>com.taobao.arthas.agent.AgentBootstrapAgent-Class>
    18. <Can-Redefine-Classes>trueCan-Redefine-Classes>
    19. <Can-Retransform-Classes>trueCan-Retransform-Classes>
    20. <Specification-Title>${project.name}Specification-Title>
    21. <Specification-Version>${project.version}Specification-Version>
    22. <Implementation-Title>${project.name}Implementation-Title>
    23. <Implementation-Version>${project.version}Implementation-Version>
    24. manifestEntries>
    25. archive>
    26. configuration>
    27. execution>
    28. executions>
    29. plugin>

    代理类、premain方法提供类编写接收Instrumentation实例的方法

    1. // 通过-agent方法显示指定方式启动JVM时会回调该方法传入Instrumentation实例
    2. public static void premain(String args, Instrumentation inst) {
    3. main(args, inst);
    4. }

    将premain提供类打成jar包。启动其他jvm时将该该jar作为agent参数出入即可,启动命令如下:

    java -javaagent "${HOME}/.arthas/lib/3.1.0/arthas/arthas-agent.jar" ...
    

    JVM启动后再启动代理方式获取

    同上中方式相同,maven的pom配置为生成的manifest文件中指定agent代码提供类 代理类方法提供类编写接收Instrumentation实例的方法

    1. // JVM启动后,通过VM.attach方式附加至目标JVM时回调该方法传入Instrumentation
    2. public static void agentmain(String args, Instrumentation inst) {
    3. main(args, inst);
    4. }

    将代理类附加到目标JVM

    1. private void attachAgent(Configure configure) throws Exception {
    2. // 根据PID获取目标JVM描述实例
    3. VirtualMachineDescriptor virtualMachineDescriptor = null;
    4. for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
    5. String pid = descriptor.id();
    6. if (pid.equals(Integer.toString(configure.getJavaPid()))) {
    7. virtualMachineDescriptor = descriptor;
    8. }
    9. }
    10. VirtualMachine virtualMachine = null;
    11. try {
    12. // 通过pid方式直接依附至目标JVM
    13. if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
    14. virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
    15. } else {
    16. // 通过JVM描述符方式依附
    17. virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
    18. }
    19. ...
    20. // 为依附上的目标JVM加载代理jar
    21. virtualMachine.loadAgent(arthasAgentPath,
    22. configure.getArthasCore() + ";" + configure.toString());
    23. ...
    24. }

    通过Instrumentation转换类字节码
    实现ClassFileTransformer接口的transform方法

    1. @Override
    2. public byte[] transform(final ClassLoader inClassLoader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    3. ...
    4. // inClassLoader:将要被转换类所在的类加载器,如果是bootstrap loader则该参数为null
    5. // className:将要被转换类的全限定名称
    6. // classBeingRedefined:将要被转换类的class
    7. // protectionDomain:将要被转换类的保护域
    8. // classfileBuffer:将要被转换类的字节码
    9. // 修改增强后的字节码
    10. return enhanceClassByteArray;
    11. ...
    12. }

    将ClassFileTransformer实现添加至Instrumentation,并执行转换

    1. // Enhancer为arthas中提供的ClassFileTransformer实现类
    2. final Enhancer enhancer = new Enhancer(adviceId, isTracing, skipJDKTrace, enhanceClassSet, methodNameMatcher, affect);
    3. // 将转换类添加至Instrumentation
    4. inst.addTransformer(enhancer, true);
    5. ...
    6. // 执行转换
    7. inst.retransformClasses(clazz);

    总结

    instrumentation.addTransformer的第二个参数canRetransform如果不指定默认为false,如果为true,则在显示调用retransformClasses时会立即回调。
    jdk api文档中指明在转换类定义之前已经实例化的对象不会受影响,这种说法指的应该是因为对象不能新增、删除、更改字段方法等,所以类型的实例不会受到重定义的影响;但是对于方法的修改会生效,官方也有说明:已经入栈的方法不会受影响,但是对于下一次方法调用则会使用新的定义
    ClassFileTransformer实例中要对类名称进行匹配,因为其他类加载时也可能会走到该转换类中导致出现异常,例如:

    1. ###### start transform
    2. loader:sun.misc.Launcher$AppClassLoader@18b4aac2, className:null, classBeingRedefined:null
    3. ###### end transform
  • 相关阅读:
    初识ElasticSearch
    122. 买卖股票的最佳时机 II
    黑马java笔记-java部分-java基础
    telnet的使用
    VMware设置共享文件夹
    C++ 入门防爆零教程(上册)
    未来中国智能制造九大趋势
    Leetcode 754. 到达终点数字
    JVM上篇之类加载子系统
    ESB优势2019-架构师(六十二)
  • 原文地址:https://blog.csdn.net/Java_ttcd/article/details/126243290