聊聊大厂都在用的 JavaAgent 上

简介: 聊聊大厂都在用的 JavaAgent 上



熟悉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时表示能够重新定义 class
  • Can-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 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

相关文章
|
6月前
|
Java Linux 应用服务中间件
Linux启动tomcat报错:Neither the JAVA_HOME nor the JRE_HOME environment variable is defined
Linux启动tomcat报错:Neither the JAVA_HOME nor the JRE_HOME environment variable is defined
249 0
|
Oracle Java 关系型数据库
java环境变量配置超详细
java环境变量配置超详细,新手
533 0
 java环境变量配置超详细
|
Java Unix Linux
M2_HOME还是MAVEN_HOME配置环境变量,有什么区别?
M2_HOME还是MAVEN_HOME配置环境变量,有什么区别?
313 0
|
Java 编译器 Maven
javaagent实战
javaagent实战
104 2
|
Java
【配置JAVA环境变量】
【配置JAVA环境变量】
60 0
|
监控 Java 应用服务中间件
APM - Hello Javaagent
APM - Hello Javaagent
120 0
|
监控 Java 程序员
聊聊大厂都在用的 JavaAgent 下
聊聊大厂都在用的 JavaAgent 下
|
Java 应用服务中间件 Windows
Java环境变量的配置与详解
本文主要讲述Java环境变量的配置方法
|
Java 应用服务中间件 API
JavaAgent-SandBox
1.前言 之前初步学习了javaAgent,并做了一份总结《JavaAgent学习笔记》。然后在看到《JVM-Sandbox 基于JVM的非侵入式运行期AOP解决方案》之后,接触到了集团的sandBox。并尝试使用这种有真正应用场景的运行时AOP框架。 2.SandBox简介 sandBox是集团开发的一种非侵入式的运行时AOP解决方案,它能动态地将你要实现的代码模块打包编织到目标代码中,实
7051 0