熟悉Spring的小伙伴们应该都对aop比较了解,面向切面编程允许我们在目标方法的前后织入想要执行的逻辑,而今天要给大家介绍的Java Agent技术,在思想上与aop比较类似,翻译过来可以被称为Java代理 、Java探针 技术。
Java Agent出现在JDK1.5版本以后,它允许程序员利用agent技术构建一个独立于应用程序的代理程序,用途也非常广泛,可以协助监测、运行、甚至替换其他JVM上的程序,先从下面这张图直观的看一下它都被应用在哪些场景:
看到这里你是不是也很好奇,究竟是什么神仙技术,能够应用在这么多场景下,那今天我们就来挖掘一下,看看神奇的Java Agent是如何工作在底层,默默支撑了这么多优秀的应用。
回到文章开头的类比,我们还是用和aop比较的方式,来先对Java Agent有一个大致的了解:
- 作用级别:aop运行于应用程序内的方法级别,而agent能够作用于虚拟机级别
- 组成部分:aop的实现需要目标方法和逻辑增强部分的方法,而Java Agent要生效需要两个工程,一个是agent代理,另一个是需要被代理的主程序
- 执行场合:aop可以运行在切面的前后或环绕等场合,而Java Agent的执行只有两种方式,jdk1.5提供的
preMain
模式在主程序运行前执行,jdk1.6提供的agentMain
在主程序运行后执行
下面我们就分别看一下在两种模式下,如何动手实现一个agent代理程序。
Premain模式
Premain模式允许在主程序执行前执行一个agent代理,实现起来非常简单,下面我们分别实现两个组成部分。
agent
先写一个简单的功能,在主程序执行前打印一句话,并打印传递给代理的参数:
public class MyPreMainAgent { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("premain start"); System.out.println("args:"+agentArgs); } }
在写完了agent的逻辑后,需要把它打包成jar
文件,这里我们直接使用maven插件打包的方式,在打包前进行一些配置。
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.cn.agent.MyPreMainAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
配置的打包参数中,通过manifestEntries
的方式添加属性到MANIFEST.MF
文件中,解释一下里面的几个参数:
Premain-Class
:包含premain
方法的类,需要配置为类的全路径Can-Redefine-Classes
:为true
时表示能够重新定义 classCan-Retransform-Classes
:为true
时表示能够重新转换 class,实现字节码替换Can-Set-Native-Method-Prefix
:为true
时表示能够设置native方法的前缀
其中Premain-Class
为必须配置,其余几项是非必须选项,默认情况下都为false
,通常也建议加入,这几个功能我们会在后面具体介绍。在配置完成后,使用mvn
命令打包:
mvn clean package
打包完成后生成myAgent-1.0.jar
文件,我们可以解压jar
文件,看一下生成的MANIFEST.MF
文件:
可以看到,添加的属性已经被加入到了文件中。到这里,agent代理部分就完成了,因为代理不能够直接运行,需要附着于其他程序,所以下面新建一个工程来实现主程序。
主程序
在主程序的工程中,只需要一个能够执行的main
方法的入口就可以了。
public class AgentTest { public static void main(String[] args) { System.out.println("main project start"); } }
在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent
参数来指定运行的代理,命令格式如下:
java -javaagent:myAgent.jar -jar AgentTest.jar
并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行:
java -javaagent:myAgent1.jar -javaagent:myAgent2.jar -jar AgentTest.jar
以我们在idea中执行程序为例,在VM options
中加入添加启动参数:
-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Hydra -javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Trunks
执行main
方法,查看输出结果:
根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。
缺陷
在提供便利的同时,premain模式也有一些缺陷,例如如果agent在运行过程中出现异常,那么也会导致主程序的启动失败。我们对上面例子中agent的代码进行一下改造,手动抛出一个异常。
public static void premain(String agentArgs, Instrumentation inst) { System.out.println("premain start"); System.out.println("args:"+agentArgs); throw new RuntimeException("error"); }
再次运行主程序:
可以看到,在agent抛出异常后主程序也没有启动。针对premain模式的一些缺陷,在jdk1.6之后引入了agentmain模式。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
Agentmain模式
agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach
机制连接两个jvm,下面我们分3个部分实现。
agent
agent部分和上面一样,实现简单的打印功能:
public class MyAgentMain { public static void agentmain(String agentArgs, Instrumentation instrumentation) { System.out.println("agent main start"); System.out.println("args:"+agentArgs); } }
修改maven插件配置,指定Agent-Class
:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class>com.cn.agent.MyAgentMain</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin>
主程序
这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in
进行阻塞,防止主进程提前结束。
public class AgentmainTest { public static void main(String[] args) throws IOException { System.in.read(); } }
attach机制
和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach
包下的VirtualMachine
工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖:
<dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${JAVA_HOME}\lib\tools.jar</systemPath> </dependency>
VirtualMachine
代表了一个要被附着 的java虚拟机,也就是程序中需要监控的目标虚拟机,外部进程可以使用VirtualMachine
的实例将agent加载到目标虚拟机中。先看一下它的静态方法attach
:
public static VirtualMachine attach(String var0);
通过attach
方法可以获取一个jvm的对象实例,这里传入的参数是目标虚拟机运行时的进程号pid
。也就是说,我们在使用attach
前,需要先获取刚才启动的主程序的pid
,使用jps
命令查看线程pid
:
11140 16372 RemoteMavenServer36 16392 AgentmainTest 20204 Jps 2460 Launcher
获取到主程序AgentmainTest
运行时pid
是16392,将它应用于虚拟机的连接。
public class AttachTest { public static void main(String[] args) { try { VirtualMachine vm= VirtualMachine.attach("16392"); vm.loadAgent("F:\\Workspace\\MyAgent\\target\\myAgent-1.0.jar","param"); } catch (Exception e) { e.printStackTrace(); } } }
在获取到VirtualMachine
实例后,就可以通过loadAgent
方法可以实现注入agent代理类的操作,方法的第一个参数是代理的本地路径,第二个参数是传给代理的参数。执行AttachTest
,再回到主程序AgentmainTest
的控制台,可以看到执行了了agent中的代码:
这样,一个简单的agentMain模式代理就实现完成了,可以通过下面这张图再梳理一下三个模块之间的关系。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能