java agent探究

2019-07-19  本文已影响0人  何轩hexuan

目前我们的业务遇到线上问题时经常需要加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在很多应用领域都发挥着重要的作用,比如:

java agent加载的时序图:

image

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

附: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,观察代码是否生效
上一篇下一篇

猜你喜欢

热点阅读