(目录)


Java Agent

背景

因在做到Skywalking Agent的时候,并没有修改程序中任何一行 Java 代码,就可无侵入式的使用组件,便使用到了 Java Agent 技术,接下来对学习学习Java Agent 技术


Java Agent 是什么

Java Agent这个技术对大多数人来说都比较陌生,但是大家都都多多少少接触过一些。

实际上我们平时用过的很多工具都是基于java Agent来实现的

例如:热部署工具JRebelspringboot的热部署插件,各种线上诊断工具btrace, greys),阿里开源的arthas等等。

java Agent在JDK1.5以后,我们可以使用agent技术构建一个独立于应用程序的代理程序(即Agent),用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能,并且这种方式一个典型的优势就是无代码侵入

image-20231115180726717

Agent分为两种:

1、在主程序之前运行的Agent,

2、在主程序之后运行的Agent(JDK 1.6以后提供)


Java Agent 使用场景

Java agent 技术结合 Java Intrumentation API 可以实现类修改、热加载等功能。

下面是 Java agent 技术的常见应用场景:

image-20231115180846906


Java Agent 开发

1. premain (主程序之前运行的Agent)

premain:主程序之前运行的Agent

image-20231114170947969

可以看到,我们的代码在转换成机器码之前,需要执行转换,加载和校验。

Java Agent即是在此过程中,对代码进行拦截,进行定制化的操作(有点AOP的意思)


ByteBuddy提供premain 方法,函数签名如下所示:

public static void premain(String args,Instrumentation inst)

顾名思义,premain 方法在main 方法之前被调用

示例如下所示:

image-20231114171254912

运行HelloWorld程序时,在HelloWorld.class被JVM加载之前,发现有premain方法对其进行拦截。

根据premain中定义的Agent规则,对HelloWorld.class执行转换(Transformed)操作,转换后的helloWorld.class文件被ClassLoader加载,功能增强完成


2. -javaagent命令

在实际使用过程中,javaagent是java命令的一个参数

通过java 命令启动我们的应用程序的时候,可通过参数 -javaagent 指定一个 jar 包(也就是我们的代理agent)

能够实现在我们应用程序的主程序运行之前来执行我们指定jar包中的特定方法,在该方法中我们能够实现动态增强Class等相关功能,并且该 jar包有2个要求:

  1. 这个 jar 包的 META-INF/MANIFEST.MF 文件必须指定 Premain-Class 项,该选项指定的是一个类的全路径
  2. Premain-Class 指定的那个类必须实现 premain() 方法。

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.itheima.PreMainAgent

pom文件

<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>com.itheima.agent.PreMainAgent</Premain-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>

重点部分关注:

Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)

Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)

Premain-Class包含 premain 方法的类(类的全路径名)

<manifestEntries>
  <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
  <Premain-Class>demo.MethodAgentMain</Premain-Class>
  <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
  <Agent-Class>demo.MethodAgentMain</Agent-Class>
  <!--Can-Redefine-Classes: 是否可进行类定义。-->
  <Can-Redefine-Classes>true</Can-Redefine-Classes>
  <!--Can-Retransform-Classes: 是否可进行类转换。-->
  <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

从字面上理解,Premain-Class 就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent所指定 jar 包内 Premain-Class 这个类的 premain 方法 。

我们可以通过在命令行输入java看到相应的参数,其中就有和java agent相关的

image-20231114171952242


3. 编写Agent demo

1、在agent-demo中添加如下坐标

<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>com.itheima.agent.PreMainAgent</Premain-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>

2、编写一个agent程序:PreMainAgent,完成premain方法的签名,先做一个简单的输出

import java.lang.instrument.Instrumentation;

public class PreMainAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("我的agent程序跑起来啦!");
        System.out.println("收到的agent参数是:"+agentArgs);
    }
}

下面先来简单介绍一下 Instrumentation 中的核心 API 方法:

  • addTransformer()/removeTransformer() 方法:注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义(修改类的字节码)
  • redefineClasses() 方法:该方法针对的是已经加载的类,它会对传入的类进行重新定义
  • **getAllLoadedClasses()方法:**返回当前 JVM 已加载的所有类
  • getInitiatedClasses() 方法:返回当前 JVM 已经初始化的类
  • getObjectSize()方法:获取参数指定的对象的大小

3、对agent-demo项目进行打包package,得到 agent-demo-1.0-SNAPSHOT.jar

4、创建agent-test项目,编写一个启动类:Application

public class Application {

    public static void main(String[] args) {
        System.out.println("main 函数 运行了 ");
    }
}

5、启动运行,添加-javaagent参数

-javaagent:/xxx.jar=option1=value1,option2=value2

image-20231114172352179

运行结果为:

我的agent程序跑起来啦!
收到的agent参数是:k1=v1,k2=v2
main 函数 运行了 

总结:

agent JVM 会先执行 premain 方法,大部分类加载都会通过该方法。

注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,bytebuddy,javassist,cglib等等来改写实现类。


4. agentmain (主程序之后运行的Agent)

agentmain:主程序之后运行的Agent

上面介绍的是在 JDK 1.5中提供的,开发者只能在main加载之前添加手脚。

Java SE 6 中提供了一个新的代理操作方法:agentmain 可以在 main 函数开始运行之后再运行

premain函数一样, 开发者可以编写一个含有agentmain函数的 Java 类,具备以下之一的方法即可

public static void agentmain (String agentArgs, Instrumentation inst)

public static void agentmain (String agentArgs)

同样需要在MANIFEST.MF文件里面设置“Agent-Class”来指定包含 agentmain 函数的类的全路径。

1、在agentdemo中创建一个新的类:AgentClass,并编写方法agenmain

public class AgentClass {

    public static void agentmain (String agentArgs, Instrumentation inst){
        System.out.println("agentmain runing");
    }
}

2:在pom.xml中添加配置如下

<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>com.itheima.agent.PreMainAgent</Premain-Class>
                <!--添加这个即可-->
                <Agent-Class>com.itheima.agent.AgentClass</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>

3:对agent-demo重新打包

4:找到agent-test中的Application,修改如下:

public class Application {

    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        System.out.println("main 函数 运行了 ");

        //获取当前系统中所有 运行中的 虚拟机
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vm : list) {
            if (vm.displayName().endsWith("com.itheima.Application")) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vm.id());
                virtualMachine.loadAgent("D:/agentdemo.jar");
                virtualMachine.detach();
            }
        }
    }
}

vlist()方法会去寻找当前系统中所有运行着的JVM进程,你可以打印vmd.displayName()看到当前系统都有哪些JVM进程在运行。

因为main函数执行起来的时候进程名为当前类名,所以通过这种方式可以去找到当前的进程id。

之所以要这样写是因为:agent要在主程序运行后加载,我们不可能在主程序中编写加载的代码

怎么另写程序如何与主程序进行通信?

这里用到的机制就是attach机制,它可以将JVM A连接至JVM B,并发送指令给JVM B执行


5. 总结

Java Agent十分强大,它能做到的不仅仅是打印几个监控数值而已,还包括使用Transformer等高级功能进行类替换,方法修改等,要使用Instrumentation的相关API则需要对字节码等技术有较深的认识。