手机版 欢迎访问it开发者社区(www.mfbz.cn)网站

当前位置: > 开发

Java Agent探针 学习笔记

时间:2021/5/23 12:42:33|来源:|点击: 次

一、前言

当我们遇到线上问题,需要加log来辅助定位问题时,通常需要改代码上线发布,才能输出log来,这样比较麻烦。

思考:是否可以在服务器上改代码让它实时生效呢?答案是可以的!

从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对象:

  • 在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.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/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功能上非常简单,仅提供了如下几个功能:

  • 列出当前所有的JVM实例描述
  • Attach到其中一个JVM上,建立通信管道
  • 让目标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/arth

七、总结

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

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

Copyright © 2002-2019 某某自媒体运营 版权所有