java中agent使用

java中agent使用

一、简介

java agent是独立于应用程序外的代理程序,可以在应用程序启动前或运行中,修改类字节码信息,改变类的行为。这里对应用程序启动前和运行中的agent使用分别介绍。

二、应用程序启动前的agent使用

应用程序启动前agent使用,是通过在应用程序启动时添加-javaagent参数(可多个-javaagent参数)实现的。

2.1 javaagent参数格式

javaagent参数使用格式如下:

java -javaagent:/xx/agent.jar[=参数]  -jar xx.jar

2.2 开发步骤

应用程序启动前agent开发,包含agent程序开发、MENIFEST.MF配置文件定义、maven中maven-jar-plugin插件修改、打jar包、主程序调用,具体开发步骤如下:

  1. 定义agent程序,需包含方法名为premain的静态方法,同时实现ClassFileTransformer接口对特定类字节码修改(结合javassist工具);

  2. 定义MENIFEST.MF配置文件,位于resources/META-INF目录下,内容类似:

    Manifest-Version: 1.0.1
    Premain-Class: com.dragon.study.spring_boot_pre_agent.PreAgentMain
    Can-Redefine-Classes: true
    
    

    其中Premain-Class为前面定义的agent类,且配置文件最要空一行。

  3. 修改mava的pom.xml中的插件配置,类似于:

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
            <archive>
                <manifest>
                    <addClasspath>true</addClasspath>
                </manifest>
                <manifestEntries>
                    <Premain-Class>
                        com.dragon.study.spring_boot_pre_agent.PreAgentMain
                    </Premain-Class>
                </manifestEntries>
            </archive>
        </configuration>
    </plugin>
    
  4. 将前面定义的agent程序打成jar包;

  5. 在目标主程序上,添加-javaagent参数及前面的agent的jar包,再运行目标主程序,类似于:

    java -javaagent:/xx/agent.jar=agentArgs  -cp /xx.jar xx.Main
    

2.3 示例

这是以spring_boot_pre_agent项目创建agent的jar包,以spring_boot_main项目中的PreAgentTargetMain为目标类为例。

2.3.1 agent项目spring_boot_pre_agent
2.3.1.1 agent项目中的maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dragon.study</groupId>
    <artifactId>spring_boot_pre_agent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring_boot_pre_agent</name>
    <description>Demo project for Spring Boot</description>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.26.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>
                                com.dragon.study.spring_boot_pre_agent.PreAgentMain
                            </Premain-Class>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
2.3.1.2 agent项目中的agent相关类

这里agent相关类包含自定义字节码编辑类ConfigTransformer.java和agent主类PreAgentMain.java,如下:
ConfigTransformer.java类如下:

package com.dragon.study.spring_boot_pre_agent;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

//修改类的字节码
public class ConfigTransformer implements ClassFileTransformer {
    private static ClassPool classPool = ClassPool.getDefault();
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        String target = "com.dragon.study.spring_boot_main.StaticConfig";
        className = className.replaceAll("/", ".");
        if(className.contains(target)){
            try {
                CtClass ctClass =  classPool.getCtClass(target);
                CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
                //指定方法添加一行自定义输出
                ctMethod.insertBefore("System.out.println(\"pre inject, configName:\"+configName);");
                //返回修改后的字节码
                return ctClass.toBytecode();
            } catch (Exception e) {
            }
        }
        //返回原类字节码
        return classfileBuffer;
    }
}

PreAgentMain类如下:

package com.dragon.study.spring_boot_pre_agent;
import java.lang.instrument.Instrumentation;
public class PreAgentMain {
    //主程序运行前执行自定义操作
    public static void premain(String agentArgs, Instrumentation inst) {
        //这里示例打印传入参数
        System.out.println("agentArgs:"+agentArgs);

        //修改指定类行为
        inst.addTransformer(new ConfigTransformer());
    }
}
2.3.1.3 agent项目中的MENIFEST.MF配置文件

MENIFEST.MF配置文件位于resources/META-INF目录下,内容为:

Manifest-Version: 1.0.1
Premain-Class: com.dragon.study.spring_boot_pre_agent.PreAgentMain
Can-Redefine-Classes: true

2.3.1.4 agent项目打为jar包

这里通过maven打jar包为:spring_boot_pre_agent-0.0.1-SNAPSHOT.jar

2.3.2 agent目标项目spring_boot_main

spring_boot_main示例包含目标相关类,以及最终添加agent启动。

2.3.2.1 目标相关类

目标相关类包含静态配置类StaticConfig.java和目标类PreAgentTargetMain.java如下:
StaticConfig.java:

package com.dragon.study.spring_boot_main;
public class StaticConfig {
    //自定义静态属性
    public static String configName="apple";

    //自定义静态方法
    public static String sayHello(){
        return "hello " + configName;
    }
}

PreAgentTargetMain.java:

package com.dragon.study.spring_boot_main.agent;
import com.dragon.study.spring_boot_main.StaticConfig;
public class PreAgentTargetMain {
    public static void main(String[] args) {
        System.out.println("main");
        //调用指定方法
        StaticConfig.sayHello();
    }
}
2.3.3 测试

应用程序启动前使用agent,是通过启动时添加-javaagent参数实现的。测试调用如下:

java -javaagent:/xx/spring_boot_pre_agent-0.0.1-SNAPSHOT.jar=agentArgs  -cp /xx/spring_boot_main-0.0.1-SNAPSHOT.jar com.dragon.study.spring_boot_main.agent.PreAgentTargetMain

输出:

agentArgs:agentArgs
main
pre inject, configName:apple

总结分析,借助应用程序启动前agent的使用,在PreAgentTargetMain启动前改变了其行为。

三、应用程序运行中的agent使用

应用程序运行中的agent使用,是通过第三方程序,借助VirtualMachine将自定义agent添加到目标程序(通过进程号pid)上。

3.1 开发步骤

应用程序运行中的agent开发和运行前类似,只是启动方式不同,包含agent程序开发、MENIFEST.MF配置文件定义、maven中maven-jar-plugin插件修改、打jar包、agent使用的目标程序、第三方启动程序,具体开发步骤如下:

  1. 定义agent程序,需包含方法名为agentmain的静态方法,同时实现ClassFileTransformer接口对特定类字节码修改(结合javassist工具);

  2. 定义MENIFEST.MF配置文件,位于resources/META-INF目录下,内容类似:

    Manifest-Version: 1.0.1
    Agent-Class: com.dragon.study.spring_boot_post_agent.PostAgentMain
    Can-Retransform-Classes: true
    Can-Redefine-Classes: true
    
    

    其中Agent-Class为前面定义的agent类,且配置文件最要空一行。

  3. 修改mava的pom.xml中的插件配置,类似于:

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>
        <configuration>
            <archive>
                <manifest>
                    <addClasspath>true</addClasspath>
                </manifest>
                <manifestEntries>
                    <Agent-Class>
                        com.dragon.study.spring_boot_post_agent.PostAgentMain
                    </Agent-Class>
                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                </manifestEntries>
            </archive>
        </configuration>
    </plugin>
    
  4. 将前面定义的agent程序打成jar包;

  5. 通过第三方程序将agent程序添加到目标程序上,类似:

    //获取指定项目运行的pid
    String targetPid = "xx"
    //运行期,对指定pid程序添加agent,动态改变程序行为
    VirtualMachine vm = VirtualMachine.attach(targetPid);
    //添加指定agent的jar包
    vm.loadAgent("/xx/agent.jar");
    vm.detach();
    

3.2 示例

这是以spring_boot_post_agent项目创建agent的jar包,以spring_boot_main项目为目标项目,以PostAgentTargetMain.java为第三方添加程序为例。

3.2.1 agent项目spring_boot_post_agent
3.2.1.1 agent项目中的maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dragon.study</groupId>
    <artifactId>spring_boot_post_agent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring_boot_post_agent</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.26.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>
                                com.dragon.study.spring_boot_post_agent.PostAgentMain
                            </Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
3.2.1.2 agent项目中的agent相关类

这里agent相关类包含自定义字节码编辑类ConfigTransformer.java和agent主类PreAgentMain.java,如下:
ConfigTransformer.java类如下:

package com.dragon.study.spring_boot_post_agent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Loader;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class ConfigTransformer implements ClassFileTransformer {
    private static ClassPool classPool = ClassPool.getDefault();
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        String target = "com.dragon.study.spring_boot_main.StaticConfig";
        className = className.replaceAll("/", ".");
        if(className.contains(target)){
            try {
                CtClass ctClass =  classPool.getCtClass(target);
                CtMethod ctMethod = ctClass.getDeclaredMethod("sayHello");
                //指定方法添加一行自定义输出
                ctMethod.insertBefore("System.out.println(\"post inject, configName:\"+configName);");
                //返回修改后的字节码
                return ctClass.toBytecode();
            } catch (Exception e) {
            }
        }
        //返回原类字节码
        return classfileBuffer;
    }
}

PostAgentMain类如下:

package com.dragon.study.spring_boot_post_agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.stream.Stream;

public class PostAgentMain {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        //这里示例打印传入参数
        System.out.println("agentArgs:" + agentArgs);

        //打印加载的所有类
        Class<?>[] clazzArr = inst.getAllLoadedClasses();
//        Stream.of(clazzArr).forEach(System.out::println);

        //打印目标项目中指定字段的内存值
        Stream.of(clazzArr).filter(t->t.getName().contains("StaticConfig")).forEach(t->{
            try {
                System.out.println("configName:"+t.getDeclaredField("configName").get(null));
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        //修改目标项目中指定字段的内存值
        Stream.of(clazzArr).filter(t->t.getName().contains("StaticConfig")).forEach(t->{
            try {
                t.getDeclaredField("configName").set(null, "banana");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        //运行期修改指定类行为
        Stream.of(clazzArr).filter(t->t.getName().contains("StaticConfig")).forEach(t->{
            try {
                inst.addTransformer(new ConfigTransformer(), true);
                inst.retransformClasses(t);
            } catch (UnmodifiableClassException e) {
                e.printStackTrace();
            }
        });
    }
}
3.2.1.3 agent项目中的MENIFEST.MF配置文件

MENIFEST.MF配置文件位于resources/META-INF目录下,内容为:

Manifest-Version: 1.0.1
Agent-Class: com.dragon.study.spring_boot_post_agent.PostAgentMain
Can-Retransform-Classes: true
Can-Redefine-Classes: true

3.2.1.4 agent项目打为jar包

这里通过maven打jar包为:spring_boot_post_agent-0.0.1-SNAPSHOT.jar

3.2.2 agent目标项目spring_boot_main

spring_boot_main为完整常规项目。

3.2.2.1 添加maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dragon.study</groupId>
    <artifactId>spring_boot_main</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring_boot_main</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.26.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
3.2.2.2 application.yaml配置
server:
  port: 10013
spring:
  application:
    name: spring-boot-main
3.2.2.3 关键类

目标相关类包含静态配置类StaticConfig.java和启动类:
StaticConfig.java:

package com.dragon.study.spring_boot_main;
public class StaticConfig {
    //自定义静态属性
    public static String configName="apple";

    //自定义静态方法
    public static String sayHello(){
        return "hello " + configName;
    }
}

启动类SpringBootMainApplication.java:

package com.dragon.study.spring_boot_main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootMainApplication {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(SpringBootMainApplication.class, args);
				//主动加载静态类
        Class.forName("com.dragon.study.spring_boot_main.StaticConfig");
    }
}
3.3.3 第三方添加类

第三方添加类是通过VirtualMachine将agent和目标项目关联起来,示例PostAgentTargetMain如下:

package com.dragon.study.spring_boot_main.agent;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;

public class PostAgentTargetMain {
    public static void main(String[] args) throws Exception {
        List<VirtualMachineDescriptor> vmList = VirtualMachine.list();
        //获取指定项目运行的pid
        String targetPid = vmList.stream().filter(t->t.displayName().endsWith("com.dragon.study.spring_boot_main.SpringBootMainApplication")).findFirst().map(t->t.id()).get();

        //运行期,对指定pid程序添加agent,动态改变程序行为
        VirtualMachine vm = VirtualMachine.attach(targetPid);
        //添加指定agent的jar包
        vm.loadAgent("/xx/spring_boot_post_agent-0.0.1-SNAPSHOT.jar");
        vm.detach();
    }
}
3.3.4 测试

启动spring_boot_main项目,运行第三方添加类PostAgentTargetMain。

java -javaagent:/xx/spring_boot_pre_agent-0.0.1-SNAPSHOT.jar=agentArgs  -cp /xx/spring_boot_main-0.0.1-SNAPSHOT.jar com.dragon.study.spring_boot_main.agent.PreAgentTargetMain

spring_boot_main输出:

agentArgs:null
configName:apple

再次通过http查看:

GET http://localhost:10013/hello/sayHello
Accept: application/json

http结果输出:

hello banana

同时spring_boot_main输出:

post inject, configName:banana

分析总结:
分析前面测试结果,可以发现,借助运行期agent的使用,实现了动态获取、修改、新增运行期StaticConfig.java(spring_boot_main项目)类的值和行为。


版权声明:本文为chinabestchina原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。