Java 调式、热部署、JVM 背后的支持者 Java Agent

我们平时写JavaAgent的机会确实不多,也可以说几乎用不着。但其实我们一直在用它,而且接触的机会非常多。下面这些技术都使用了JavaAgent技术,看一下你就知道为什么了。-各个JavaIDE的调试功能,例如eclipse、IntelliJ;-热部署功能,例如JRebel、XRebel、spring-loaded;-各种线上诊断工具,例如Btrace、Greys,还有阿里的Arthas;-各种...

Java 调式、热部署、JVM 背后的支持者 Java Agent

我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着。但其实我们一直在用它,而且接触的机会非常多。下面这些技术都使用了 Java Agent 技术,看一下你就知道为什么了。

-各个 Java IDE 的调试功能,例如 eclipse、IntelliJ ;

-热部署功能,例如 JRebel、XRebel、 spring-loaded;

-各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas;

-各种性能分析工具,例如 Visual VM、JConsole 等;

Java Agent 直译过来叫做 Java 代理,还有另一种称呼叫做 Java 探针。首先说 Java Agent 是一个 jar 包,只不过这个 jar 包不能独立运行,它需要依附到我们的目标 JVM 进程中。我们来理解一下这两种叫法。

代理:比方说我们需要了解目标 JVM 的一些运行指标,我们可以通过 Java Agent 来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标 JVM ,但是我们是通过 Java Agent 来获取的,对于目标 JVM 来说,它就像是一个代理;

探针:这个说法我感觉非常形象,JVM 一旦跑起来,对于外界来说,它就是一个黑盒。而 Java Agent 可以像一支针一样插到 JVM 内部,探到我们想要的东西,并且可以注入东西进去。

拿上面的几个我们平时会用到的技术举例子。拿 IDEA 调试器来说吧,当开启调试功能后,在 debugger 面板中可以看到当前上下文变量的结构和内容,还可以在 watches 面板中运行一些简单的代码,比如取值赋值等操作。还有 Btrace、Arthas 这些线上排查问题的工具,比方说有接口没有按预期的返回结果,但日志又没有错误,这时,我们只要清楚方法的所在包名、类名、方法名等,不用修改部署服务,就能查到调用的参数、返回值、异常等信息。

上面只是说到了探测的功能,而热部署功能那就不仅仅是探测这么简单了。热部署的意思就是说再不重启服务的情况下,保证最新的代码逻辑在服务生效。当我们修改某个类后,通过 Java Agent 的 instrument 机制,把之前的字节码替换为新代码所对应的字节码。

Java Agent 结构


Java Agent 最终以 jar 包的形式存在。主要包含两个部分,一部分是实现代码,一部分是配置文件。

配置文件放在 META-INF 目录下,文件名为 MANIFEST.MF 。包括以下配置项:

Manifest-Version: 版本号
Created-By: 创作者
Agent-Class: agentmain 方法所在类
Can-Redefine-Classes: 是否可以实现类的重定义
Can-Retransform-Classes: 是否可以实现字节码替换
Premain-Class: premain 方法所在类

入口类实现 agentmainpremain 两个方法即可,方法要实现什么功能就由你的需求决定了。

Java Agent 实现和使用

接下来就来实现一个简单的 Java Agent,基于 Java 1.8,主要实现两点简单的功能:

1、打印当前加载的所有类的名称;

2、监控一个特定的方法,在方法中动态插入简单的代码并获取方法返回值;

在方法中插入代码主要是用到了字节码修改技术,字节码修改技术主要有 javassist、ASM,已经 ASM 的高级封装可扩展 cglib,这个例子中用的是 javassist。所以需要引入相关的 maven 包。

<dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.12.1.GA</version></dependency>

实现入口类和功能逻辑

入口类上面也说了,要实现 agentmainpremain 两个方法。这两个方法的运行时机不一样。这要从 Java Agent 的使用方式来说了,Java Agent 有两种启动方式,一种是以 JVM 启动参数 -javaagent:xxx.jar 的形式随着 JVM 一起启动,这种情况下,会调用 premain方法,并且是在主进程的 main方法之前执行。另外一种是以 loadAgent 方法动态 attach 到目标 JVM 上,这种情况下,会执行 agentmain方法。

加载方式执行方法
-javaagent:xxx.jar 参数形式premain
动态 attachagentmain

代码实现如下:

package kite.lab.custom.agent;import java.lang.instrument.Instrumentation;public class MyCustomAgent { /**  * jvm 参数形式启动,运行此方法  * @param agentArgs  * @param inst  */ public static void premain(String agentArgs, Instrumentation inst){  System.out.println(“premain“);  customLogic(inst); } /**  * 动态 attach 方式启动,运行此方法  * @param agentArgs  * @param inst  */ public static void agentmain(String agentArgs, Instrumentation inst){  System.out.println(“agentmain“);  customLogic(inst); } /**  * 打印所有已加载的类名称  * 修改字节码  * @param inst  */ private static void customLogic(Instrumentation inst){  inst.addTransformer(new MyTransformer(), true);  Class[] classes = inst.getAllLoadedClasses();  for(Class cls :classes){System.out.println(cls.getName());  } }}

我们看到这两个方法都有参数 agentArgs 和 inst,其中 agentArgs 是我们启动 Java Agent 时带进来的参数,比如-javaagent:xxx.jar agentArgs。Instrumentation Java 开放出来的专门用于字节码修改和程序监控的实现。我们要实现的打印已加载类和修改字节码也就是基于它来实现的。其中 inst.getAllLoadedClasses()一个方法就实现了获取所以已加载类的功能。

inst.addTransformer方法则是实现字节码修改的关键,后面的参数就是实现字节码修改的实现类,代码如下:

public class MyTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {  System.out.println(“正在加载类:“  className);  if (!“kite/attachapi/Person“.equals(className)){return classfileBuffer;  }  CtClass cl = null;  try {ClassPool classPool = ClassPool.getDefault();cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));CtMethod ctMethod = cl.getDeclaredMethod(“test“);System.out.println(“获取方法名称:“  ctMethod.getName());ctMethod.insertBefore(“System.out.println(\“ 动态插入的打印语句 \“);“);ctMethod.insertAfter(“System.out.println($_);“);byte[] transformed = cl.toBytecode();return transformed;  }catch (Exception e){e.printStackTrace();  }  return classfileBuffer; }}

以上代码的逻辑就是当碰到加载的类是 kite.attachapi.Person的时候,在其中的 test 方法开始时插入一条打印语句,打印内容是“动态插入的打印语句“,在test方法结尾处,打印返回值,其中$_就是返回值,这是 javassist 里特定的标示符。

MANIFEST.MF 配置文件

在目录 resources/META-INF/ 下创建文件名为 MANIFEST.MF 的文件,在其中加入如下的配置内容:

Manifest-Version: 1.0Created-By: fengzhengAgent-Class: kite.lab.custom.agent.MyCustomAgentCan-Redefine-Classes: trueCan-Retransform-Classes: truePremain-Class: kite.lab.custom.agent.MyCustomAgent

配置打包所需的 pom 设置

最后 Java Agent 是以 jar 包的形式存在,所以最后一步就是将上面的内容打到一个 jar 包里。

在 pom 文件中加入以下配置

<build> <plugins>  <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><configuration> <archive>  <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> <descriptorRefs>  <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs></configuration>  </plugin> </plugins></build>

用的是 maven 的 maven-assembly-plugin 插件,注意其中要用 manifestFile 指定 MANIFEST.MF 所在路径,然后指定 jar-with-dependencies ,将依赖包打进去。

上面这是一种打包方式,需要单独的 MANIFEST.MF 配合,还有一种方式,不需要在项目中单独的添加 MANIFEST.MF 配置文件,完全在 pom 文件中配置上即可。

<build> <plugins>  <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><executions> <execution>  <goals><goal>attached</goal>  </goals>  <phase>package</phase>  <configuration><descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive> <manifestEntries>  <Premain-Class>kite.agent.vmargsmethod.MyAgent</Premain-Class>  <Agent-Class>kite.agent.vmargsmethod.MyAgent</Agent-Class>  <Can-Redefine-Classes>true</Can-Redefine-Classes>  <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> 
源文地址:https://www.guoxiongfei.cn/cntech/26796.html