0%

java的agent探针技术

java的agent探针技术

介绍

Java Agent直译过来就是java代理或者有的地方叫做java探针。这个jar包不能独立运行,需要依附于目标JVM进程中。主要作用是通过对JVM进程进行代理,可以在目标JVM运行过程中获取到对应虚拟机中相关参数。

​ 主要应用场景为:热部署、IDEADEBUG调试功能,Arthas线上诊断工具、性能监控等。

使用

java Agent有两个入口,两个入口的执行时机有所不同,一个是在目标应用启动之前,一个是在目标应用运行过程中。

目标应用刚启动时:

​ 通过在目标应用的启动参数中添加-javaagent:xxx/xxx/xxxAgent.jar的方式启动,在执行目标应用的main方法之前会进入到xxxAgent.jarpremain()方法中。

1
2
3
4
5
6
7
// 如果两个方法都存在的情况下,会进入这个方法,不会进入单个参数的方法。
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("------ premain方法,有一个入参 ------ agentArgs:" + agentArgs);
}
public static void premain(String agentArgs){
System.out.println("------ premain方法,有一个入参 ------ agentArgs:" + agentArgs);
}

注意:这种方式是在目标应用要执行main方法,之前进入这个premain方法,也就是说这时jvm虚拟机中并没有加载过多的类。由于这种加载逻辑,导致在基本上在类加载之后就无法修改字节码,所以这种方式存的灵活性存在一定的限制。

目标应用运行过程中:

​ 通过中间程序动态附着到目标JVM中的方式启动,所以中间程序就可以做到动态化,也就是达到某个条件之后就附着。

agent.jar

1
2
3
4
5
6
7
// 如果两个方法都存在的情况下,会进入这个方法,不会进入单个参数的方法。
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("------ agentmain方法 有两个入参 ------ agentArgs:" + agentArgs + " inst:" + inst.toString());
}
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("------ agentmain方法 有两个入参 ------ agentArgs:" + agentArgs);
}

BridgeProject(中间程序):

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws InterruptedException, AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException {
VirtualMachine vm = null;
for(VirtualMachineDescriptor vmDescriptor : VirtualMachine.list()){
if(vmDescriptor.displayName().contains("XxxBootstrap")){
System.out.println("进入附着jvm");
vm = VirtualMachine.attach(vmDescriptor);
}
}
vm.loadAgent("xxx/xxx/agent.jar");
}

也就是说拥有XxxBootstrap启动类的目标应用当前正处在运行状态,然后启动中间程序BridgeProject#main方法,它会获取XxxBootstrap对应的虚拟机之后,将agent.jar附着到该虚拟机上,这时就会进入到agent.jar包中的agentmain方法,就可以动态的进行一些处理。

Arthas就是使用agentmain这种方式,它在首次使用的时候是仅下载这个中间程序curl -O https://arthas.aliyun.com/arthas-boot.jar,当通过java -jar arthas-boot.jar命令启动这个springboot程序之后,它会罗列当前环境中的虚拟机(如果你开多个应用,会有多个虚拟机)。当你选中某个应用之后,它会校验arthasHome=~/.arthas/lib/这个目录是否有agent.jar包,如果没有它会去下载agent.jar包,当然它的逻辑会更复杂,它不单单只有agent.jar包。也就是说实际上通过curl下载的arthas-boot.jar实际上是对上述中间程序的一个封装,做一些外围非核心逻辑的处理,当选中某个应用时,再去下载核心agent.jar包,进行探针修改字节码相关逻辑处理。

Instrumentation相关接口:

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
/**
* 增加一个Class文件的转换器,该转换器用于改变class二进制流的数据,
* 参数canRetransform设置是否允许重新转换。
*/
addTransformer();

/**
* 类加载之前,重新定义class文件,ClassDefinition表示一个类新的定义,
* 如果在类加载之后,需要用retransformClasses方法重新定义。
*/
redefineClasses();

/**
* 在类加载之后,重新定义class。事实上,该方法update了一个类。
*/
retransformClasses();

/**
* 添加jar文件到BootstrapClassLoader中。
*/
appendToBootstrapClassLoaderSearch();

/**
* 添加jar文件到system class loader。
*/
appendToSystemClassLoaderSearch();

/**
* 获取加载的所有类数组。
*/
getAllLoadedClasses();

注意点

1.打包

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
33
34
35
36
37
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive> <!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<!-- 添加 mplementation-*和Specification-*配置项-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<!--指定premain方法所在的类-->
<Premain-Class>cn.com.xiaocainiaoya.Agent</Premain-Class>
<!--添加这个即可-->
<Agent-Class>cn.com.xiaocainiaoya.Agent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

注意这里的descriptorRef这是会将pom中的依赖打入到agent.jar包中,所以是否启动这个要取决于你的探针中需要的类库在目标程序中是否存在。比如我的目标程序中不存在directory-watcher依赖,但是agent.jar是需要这个依赖,所以我这里使用descriptorRef将这个依赖也打入到最终的agent.jar包中,如果不希望部分依赖打入到agent.jar包中(如果主工程中也存在这个依赖,会出现冲突的情况),需要在pom.xml的依赖项中添加<scope>标签。

1
2
3
4
5
6
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
<scope>provided</scope>
</dependency>

2.tool.jar工具

其中VirtualMachineJDK工具包下的类,如果系统环境变量没有配置,需要自己在Maven中引入本地文件。一般采用方案二时,动态附着需要使用到这些类。

1
2
3
4
5
6
7
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/lib/tools.jar</systemPath>
</dependency>

3.ClassFileTransformer

通过实现这个类ClassFileTransformer,然后通过Instrumentation添加到类转换器中,即可对类进行动态修改。

1
inst.addTransformer(new ProfilingTransformer());
-------------本文结束感谢您的阅读-------------