美文网首页
java agent探究

java agent探究

作者: 何轩hexuan | 来源:发表于2019-07-19 13:38 被阅读0次

    目前我们的业务遇到线上问题时经常需要加log调试。从加一行log到push代码再到jekens编译、打包、最后再部署,这个过程时间消耗非常长,而且各个环节都可能出现其他因素干扰(如如多人同时提交导致代码冲突编译不过等),造成的时间消耗就更长了。甚至有时候需要逐步加日志排查问题,重复很多次改代码再打包部署的操作,实在是费心费力。。。
    可不可以在服务器上直接改代码使之实时生效?结论是可以的。

    一、java Instrumentation

    从java5开始,jdk中新增了一个java.lang.instrument.Instrumentation 类,它提供在运行时重新加载某个类的的class文件的api。下面是它的一些主要api

    public interface Instrumentation {
    /**
         * 加入一个转换器Transformer,之后的所有的类加载都会被Transformer拦截。
         * ClassFileTransformer类是一个接口,使用时需要实现它,该类只有一个方法,该方法传递类的信息,返回值是转换后的类的字节码文件。
         */
        void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    
    
     /**
         * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
         * 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
         */
        void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
        
    /**
       *此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
       *在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
       *该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
       */
        void redefineClasses(ClassDefinition... definitions)throws  ClassNotFoundException, UnmodifiableClassException;
    
        /**
         * 获取一个对象的大小
         */
        long getObjectSize(Object objectToSize);
        
        /**
         * 将一个jar加入到bootstrap classloader的 classpath里
         */
        void appendToBootstrapClassLoaderSearch(JarFile jarfile);
        
        /**
         * 获取当前被JVM加载的所有类对象
         */
        Class[] getAllLoadedClasses();
    }
    

    通过addTransformer可以加入一个转换器,转换器可以实现对类加载的事件进行拦截并返回转换后新的字节码,通过redefineClasses或retransformClasses都可以触发类的重新加载事件。通过这几个方法的组合,就可以实现文章开头提到的不修改代码使之实时生效的目的了。

    二、JAVA Agent

    通过操作Instrumentation的api就可以实现不重启服务对单个类进行简单的修改。Instrumentation是一个interface,它的实现类InstrumentationImpl只有一个private的构造方法。
    怎么拿到这个对象呢?下面是Instrumentation类的一段注释说明:



    有两种方式拿到Instrumentation对象:
    在jvm启动时指定agent,Instrumentation对象会通过agent的premain方法传递。
    在jvm启动后通过jvm提供的机制加载agent,Instrumentation对象会通过agent的agentmain方法传递。

    三、实践java启动时加载agent 获取Instrumentation对象

    编写agent类并编译成.class文件,之后把它打成jar包,然后在jvm启动参数中指定jar包位置,具体操作步骤:
    1、创建一个agent类,并创建premain方法,premain方法的参数是固定的。

    public class preMainAgentClz {
        private static Instrumentation instrumentation;
        public static void premain(String agentArgs, Instrumentation inst) {
            instrumentation = inst;
            System.err.println("com.hexuan.agent.demo1.preMainAgentClz 我在main启动之前启动");
        }
    }
    

    2、指定premain方法的位置(两种指定方式,设置一种就行)
    方式1)创建并编辑 resources/META-INF/MANIFEST.MF 文件,当打jar包时将该文件一并打包

    Premain-Class: com.hexuan.agent.demo1.preMainAgentClz #premain方法所在类的位置
    

    方式2)如果是maven项目,在pom.xml加入

     <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                            </manifest>
                            <manifestEntries>
                                <Premain-Class>com.hexuan.agent.demo1.preMainAgentClz</Premain-Class>
                                <Agent-Class>com.hexuan.agent.demo1.agentMainAgentClz</Agent-Class>
                            </manifestEntries>
                        </archive>
                    </configuration>
                </plugin>
    

    3、如果是在pom中配置的,直接maven package就好了。如果是MANIFEST.MF文件指定的方式,将包含premain的类编译成class文件,并和MANIFEST.MF一起文件打包jar。
    4、启动时指定agent位置,在jvm启动参数中加入-javaagent参数并指定jar文件位置。

    -javaagent:/Users/hexuan/IdeaProjects/acfun_WorkSpace/java-agent-demo/target/java-agent-demo-1.0-SNAPSHOT.jar
    

    5、启动java,agent的premain方法会在main方法之前执行。

    四、在java启动后以attach的方式加载agent

    上文介绍了java进程启动时加载agent的方式和步骤,通过它在启动之前将指定的类进行替换。但如果要实现文章开头提到的调试线上代码,我们需要在修改了class文件后重启jvm并且设置-javaagent参数,显然这种方式不是我们最想要的。上文提到过我们可以在jvm启动后通过jvm提供的机制加载agent,也就是说我们能够在任何时候去加载agent,然后替换类文件。这个机制就是jdk的attach api。

    Attach API是Sun公司提供的一套扩展API,用来向目标JVM"附着"(Attach)代理工具程序的。有了它,开发者可以方便的监控一个JVM,运行一个外加的代理程序,Sun JVM Attach API功能上非常简单,仅提供了如下几个功能:

    1. 列出当前所有的JVM实例描述
    2. Attach到其中一个JVM上,建立通信管道
    3. 让目标JVM加载Agent

    Attach Api 对应的代码位置在 com.sun.tools.attach 包,包里边有一个类VirtualMachine,它有两个比较重要方法:

    /**
      *传递一个进程号作为参数,返回目标jvm进程的vm对象。
      *该方法其实是JVM进程之间指令传递的桥梁,底层通过socket进行通信。
      *JVM A可以发送一些指令给JVM B,B收到指令之后,可以执行对应的逻辑
      * 比如在命令行中经常使用的jstack、jcmd、jps等,很多都是基于这种机制实现的
      **/
    public static VirtualMachine attach(String var0) throws AttachNotSupportedException, IOException 
    
    /**
      *该方法允许我们将agent对应的jar文件地址作为参数传递目标jvm
      *目标jvm收到该命令后会加载这个agent
      **/
    public void loadAgent(String var1) throws AgentLoadException, AgentInitializationException, IOException
    

    显然,我们可以创建一个java进程,用它attach到对应的jvm,并加载agent,agent加载后我们的类也就被成功替换了。

    五、怎么得到新的类文件

    Instrumentation操作的是.class文件,对于我们开发人员来讲,我们看不懂.class文件,更无法直接修改它了。还是考虑文章一开始提到的线上改代码调试的场景,我们知道了如何去替换类,但是如何得到新的.class类文件呢?
    方式1:线下修改.java文件 -->编译成.class文件 -->上传到线上机器-->instrument
    方式2:线上.class旧文件 -->反编译成.java文件 -->修改java文件 -->编译成.class文件 -->instrument
    方式3:通过ASM或其他操作字节码的组件直接修改.class文件-->instrument
    ...
    无论哪种方式,流程太复杂容易出错,有成熟的组件吗?有,Arthas和Btrace

    六、Arthas&Btrace

    BTrace 是基于动态字节码修改技术(Instrumentation)来实现运行时 java 程序的跟踪和替换。大体的原理可以用下面的公式描述:Client(Java compile api + attach api) + Agent(脚本解析引擎 + ASM + JDK6 Instumentation) + Socket其实 BTrace 就是使用了 java attach api 附加 agent.jar ,然后使用脚本解析引擎+asm来重写指定类的字节码,再使用 instrument 实现对原有类的替换。
    但是BTrace脚本在使用上有一定的学习成本,如果能把一些常用的功能封装起来,对外直接提供简单的命令即可操作的话,那就再好不过了。2018年9月份阿里开源了自己的Java诊断工具Arthas。Arthas功能非常强大,通过简单的命令行操作即可完成对应功能。究其背后的技术原理,和本文中提到的大致无二。

    Btrace开源地址:https://github.com/btraceio/btrace
    Arthas开源地址:https://github.com/alibaba/arthas

    七、总结

    java instrument在很多应用领域都发挥着重要的作用,比如:

    • apm:(Application Performance Management)应用性能管理。pinpoint、cat、skywalking等都基于Instrumentation实现
    • idea的HotSwap、Jrebel等热部署工具
    • 应用级故障演练
    • Java诊断工具Arthas、Btrace等

    java agent加载的时序图:

    image

    附1:java Instrumentation的redefineClasses 和retransformClasses 的补充说明:

    • 二者的区别:都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。
    • 相互依赖的类加载: 允许传类集合,以满足类之间相互依赖的情况,加载顺序为集合顺序
    • 替换后生效时机:如果一个被修改的方法已经在栈桢中存在,则栈桢中的会使用旧字节码定义的方法继续运行,新字节码会在新栈桢中执行
    • 不修改变量值:该方法不会导致类的一些初始化方法执行、不会修改静态变量的值
    • 只改变方法体:该方法可以改变类的方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
    • 字节码有问题时不加载:在类转化前该方法不会check字节码文件,如果结果字节码出错了,该方法将抛出异常。如果该方法抛出异常,则不会重新定义任何类

    附:2、使用Arthas实现加log调试

    #下载arthas agent
    wget https://alibaba.github.io/arthas/arthas-boot.jar
    
    #启动agent
    java -jar arthas-boot.jar --target-ip 0.0.0.0
    
    #sc:search class 查找类文件
    sc *SelectionController
    
    #jad 反编译class 并输出到文件
    jad --source-only com.acfun.controller.SelectionController > /tmp/SelectionController.java
    
    #修改源代码
    vi /tmp/SelectionController.java
    
    #sc查找加载UserController的ClassLoader  -d参数可以打印出类加载的具体信息
    sc -d *SelectionController |grep classLoaderHash 
    
    #编译源代码 使用mc(Memory Compiler)命令来编译,并且通过-c参数指定ClassLoader
    mc -c 3787f831 /tmp/SelectionController.java -d /tmp
    
    #使用redefine命令重新加载新编译好的class
    redefine /tmp/com/acfun/controller/SelectionController.class
    
    #redefine成功之后,访问controller,观察代码是否生效
    

    相关文章

      网友评论

          本文标题:java agent探究

          本文链接:https://www.haomeiwen.com/subject/dyijlctx.html