Java Agent

什么是 Java Agent

是通过实现 Java 的 Instrumentation 接口, 实现对 Java 程序启动前或者启动后的一系列操作.

启动时拦截main的Agent

使用方式是在启动相应 jar 时加入启动参数 -javaagent: 使用的拦截jar

拦截jar所需满足的条件

  • META-INF 目录下的 MANIFEST.MF 文件中必须指定 Premain-Class 配置项
  • premain-class 配置项指定的类必须提供了 premain() 方法

其中 permain 方法有两种重载

1
2
public static void premain(String agentArgs, Instrumentation inst); [1]
public static void premain(String agentArgs); [2]

假如两个方法同时存在指定的 premain-class 中,那么将会调用[1]的方法

具体项目可以查看这个示例: 项目地址

启动后进行增强

使用VirtualMachineattach方法进行附着到指定 Pid 的 Java 虚拟机上, 并使用 loadAgent方法加载指定的jar

拦截jar所需满足的条件

  • META-INF 目录下的 MANIFEST.MF 文件中必须指定 Agent-Class 配置项
  • Agent-Class 配置项指定的类必须提供了 agentmain() 方法

其中 agentmain 方法有两种重载

1
2
public static void agentmain(String agentArgs, Instrumentation inst); [1]
public static void agentmain(String agentArgs); [2]

同样的, 同时存在时, 仅会调用[1]方法
附着jar项目
进行附着的项目

能干啥

在使用了 Agent 后, 除了能在main方法或者在附着的时候执行某些代码之外, 还能通过Instrumentation对已有的 Class 进行修改

引入编译时的 Maven 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<Premain-Class>io.whileaway.example.PremainDemo</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<!-- 绑定到package生命周期阶段上 -->
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

对已有类进行简单替换(使用premain进行举例)

首先创建一个想要被修改的类

1
2
3
4
5
6
public class PremainTarget {

public Integer getOne() {
return 1;
}
}

将编译后的类移入premain-demo项目的resource

再修改getOne的返回值为2
1
2
3
public Integer getOne() {
return 2;
}

启动时再在启动参数中添加-javaagent参数

最后结果输出

1
2
3
use Premain agent
premain done
1

使用ByteBuddy进行修改

除了使用Class进行替换整个类文件,还可以借助其他工具进行简便的操作类,例如ByteBuddy.
主要用于协助字节码屏蔽/简化复杂Instrumentation API操作/提供类型安全的 API 和注解
让我们能够轻松进行字节码操作.

引入maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.11.8</version>
</dependency>

<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.11.8</version>
</dependency>

在premain方法中指定Agent处理

下面简单的实现一个统计某个类的所有方法的耗时Agent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void premain(String agentArgs, Instrumentation inst) throws Exception{
// 对增强类中更细致的处理方式
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, module) -> {
// method 指定拦截的方法需要, ElementMatchers.any() 拦截所有方法
return builder.method(ElementMatchers.any())
// intercept() 指明方法的处理类
.intercept(MethodDelegation.to(TimeKeeperInterceptor.class));
};
// 使用 AgentBuild 定义处理的方式
new AgentBuilder
.Default()
// 根据包名前缀拦截类
.type(ElementMatchers.nameStartsWith("io.whileaway"))
// 拦截到的类由 transformer 处理
.transform(transformer)
// 安装到 Instrumentation
.installOn(inst);
}

public class TimeKeeperInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
try {
return callable.call(); // 执行原函数
} finally {
System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
}
}
}

参考链接

  1. Guide to Java Instrumentation
  2. ByteBuddy
Author: Sean
Link: https://blog.whileaway.io/posts/7232cb06/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.