初探Java agent技术
前言
不知道各位小伙伴在此之前,是否有听过或者了解过agent相关技术,没有听说过也没有关系,我们今天的目的就是介绍agent的相关技术,探讨agent的应用场景,分享一些实际开发中的应用案例。
印象中,我第一次了解agent技术,是在分享skywalking这款工具的时候,skywalking与我们项目的绑定就是通过agent来实现的。好了,先说这么多,接下来我们就来详细介绍下agent的一些技术点。
Agent
Agent是什么
Agent中文含义代理,但是在java中我更喜欢称它为探针而非代理,尽管他也属于代理技术,但是代理本身并不能体现agent的作用。
agent技术是在JDK1.5引入的,通过agent技术,我们可以构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能。
Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供),稍后我们会有具体实例展示。
Agent能干什么
首先它最大的作用就是解耦,比如skywalking中的应用,我们不需要对我们的程序做任何修改,只需要通过agent技术引入skywalking的代理包即可;其次最常应用的场景就是jvm级的AOP,比如jvm的监测;另一种就是类似热部署这样的字节码动态操作。
Agent技术演示
说了这么多好多小伙伴肯定看的云里雾里的,接下来我们通过两个简单示例,来演示下Agent技术的神奇之处。
先看第一种,也就是在主程序之前运行的Agent。
在主程序之前运行的Agent
首先我们创建一个maven项目,编写这样一个Agent类:
import java.lang.instrument.Instrumentation;
/**
* 在主程序之前运行的Agent
*/
public class PremainAgent {
public static void premain(String preArgs, Instrumentation instrumentation) {
System.out.println("premainAgent.premain start");
System.out.println("preArgs: " + preArgs);
Class[] allLoadedClasses = instrumentation.getAllLoadedClasses();
for (Class allLoadedClass : allLoadedClasses) {
System.out.println("premainAgent LoadedClass: " + allLoadedClass.getName());
}
}
}
这里的方法名和参数列表是固定的,根据方法名我们能看出这个方法应该是运行在main方法之前的,等下测试下就知道了。
接着,我们在pom.xml文件中增加如下内容:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>io.github.syske.agent.PremainAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
上面这些内容是配置我们构建时生成的MANIFEST文件,通常我们打的jar包都有这个文件。最核心的配置就是Premain-Class,这里配置的是我们探针的类名,如果没有这个配置,我们的premain方法是不会被识别的。

然后我们通过maven把我们当前项目打成一个jar包,打完包之后的jar文件如上图,打开MANIFEST.MF文件,你会发现我们指定的Premain-Class也被写入了,这时候我们的包就是打好了,下面就是运行测试了。

运行也很简单,只需要找到一个可运行的jar包,比如一个springboot项目的包,然后在jar文件的启动命令中,增加如下配置即可:
--javaagent:你的agent文件完整路径/agent文件名.jar
# 例如我的:D:\workspace\learning\example-everyday\example-2021.07.02\target\example-2021.07.02-1.0-SNAPSHOT.jar
这里我用之前的一个springboot项目演示:
java -javaagent:D:\workspace\learning\example-everyday\example-2021.07.02\target\example-2021.07.02-1.0-SNAPSHOT.jar -jar
大家注意,在javaagent和agent文件之间不能有空格,否则会报如下错误

如果你的配置和启动命令都没有问题,在启动控制台应该会显示如下信息:

我们可以看到premain方法在我们springboot项目启动前被执行了,但是preArgs是null,这是由于我们没有注入参数,所以显示为空,我们可以通过这样的方式为preArgs注入参数:
java -javaagent:D:\workspace\learning\example-everyday\example-2021.07.02\target\example-2021.07.02-1.0-SNAPSHOT.jar=syske -jar .\springboot-learning-0.0.1-SNAPSHOT.jar
也就是在我们的agent包后面直接=需要注入的参数值就可以了,然后再次执行你会发现参数已经被注入了:

关于Instrumentation这个参数,今天先不讲了,我们说的字节码操都是基于这个参数进行操作的。下面我们看下第二种Agent
在主程序之后运行的Agent
相比第一种agent,第二种是在main方法启动后运行agent方法,而且这种方式应用最广泛,比如我们前面说的热部署,就是这种方式实现的,下来我们看下具体如何实现。
第一步,也是写Agent实现类:
public class AgentMain {
public static void agentmain(String args, Instrumentation instrumentation) {
System.out.println("AgentMainTest.agentmain start");
System.out.println("args: " + args);
Class[] allLoadedClasses = instrumentation.getAllLoadedClasses();
// for (Class allLoadedClass : allLoadedClasses) {
System.out.println("AgentMainTest LoadedClass: " + allLoadedClasses[0].getName());
// }
}
}
和第一种agent不一样的只有方法名,连参数都一模一样,这里为了方便查看,我只打印了一行数据。然后我们还需要修改下maven的打包配置,需要把之前的Premain-Class标签改成Agent-Class,其他都一样:

然后再打包,但是这一次运行方式和第一次不一样,这里的agent要通过代码来启动。
我们创建一个测试类,写一个main方法,因为要用到tools包下的类,所以要先引入tools包:

测试类如下:
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
public class MainTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> machineDescriptorList = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : machineDescriptorList) {
if ("io.github.syske.agent.MainTest".equals(virtualMachineDescriptor.displayName())) {
String id = virtualMachineDescriptor.id();
VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent("D:\\workspace\\learning\\example-everyday\\example-2021.07.02\\target\\example-2021.07.02-1.0-SNAPSHOT.jar",
"syske agentmain");
virtualMachine.detach();
}
}
System.out.println("MainTest start");
}
}
这里解释下,VirtualMachine.list()是获取当前运行的所有jvm虚拟机,运行结果如下:

其中的VirtualMachineDescriptor包含如下信息:

我们需要从中拿出displayName为io.github.syske.agent.MainTest,也就是当前类的虚拟机描述信息,然后根据虚拟机id拿到对应虚拟机,然后为该虚拟机加载Agent包,同时我们还在加在Agent包的同时,传入了syske agentmain参数,这里的参数和我们第一种方式=的方式类似,就相当于给args赋值,然后断开虚拟机连接。
运行代码,结果如下:

根据运行结果,我们发现这种Agent并发是在main方法之后执行,而是可以在你任意需要的地方调用。相比于第一种,确实要灵活一些。
总结
Agent其实在日常开发中经常用到,但是由于我们大部分情况下都用的是集成开发环境,所以感知不强,像日志采集、热部署等基本上都是基于Agent来实现的。
当然,agent最大的好处在于,它可以有效解耦,实现jvm层面的AOP,而且它又支持字节码操作,如果你玩的够溜,你就可以实现更多强大功能,而且可玩性还高,简直可以为所欲为。
今天我们暂时就讲这么多,后面抽时间用agent实现一些具体的功能,比如字节码操作,让大家真正见识Agent的强大之处。
