JavaAgent探针


一、介绍

Agent中文含义代理,但是在java中我跟喜欢称它为探针而非代理,尽管他也属于代理技术,但是代理本身并不能体现agent的作用。

agent技术是在JDK1.5引入的,通过agent技术,我们可以构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他JVM上的程序。使用它可以实现虚拟机级别的AOP功能。

Agent分为两种,一种是在主程序之前运行的Agent,一种是在主程序之后运行的Agent(前者的升级版,1.6以后提供)

agent 的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略 (security policy) 和上下文 (context) 所管理。

Java Agent 这个技术,对于大多数同学来说都比较陌生,但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于Java Agent实现的,例如常见的热部署 JRebel,各种线上诊断工具(btrace, greys),还有阿里开源的线上诊断工具 arthas,分布式链路追踪 Skywalking

其实Java Agent一点都不神秘,也是一个Jar包,只是启动方式和普通Jar包有所不同,对于普通的Jar包,通过指定类的main函数进行启动,但是Java Agent并不能单独启动,必须依附在一个Java应用程序运行,有点像寄生虫的感觉。

Agent能干什么

首先它最大的作用就是解耦,比如skywalking中的应用,我们不需要对我们的程序做任何修改,只需要通过agent技术引入skywalking的代理包即可;其次最常应用的场景就是jvm级的AOP,比如jvm的监测;另一种就是类似热部署这样的字节码动态操作。

二、自定义JavaAgent

1、Agent启动类

public class AppAgent {

    /**
     * 以vm参数的方式载入,在Java程序的main方法执行之前执行
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("AppAgent.premain start");
        System.out.println("agentArgs: " + agentArgs);
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class allLoadedClass : allLoadedClasses) {
            System.out.println("premainAgent LoadedClass: " + allLoadedClass.getName());
        }
    }
    /**
     * 以Attach的方式载入,在Java程序启动后执行
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("AppAgent.agentmain start");
        System.out.println("agentArgs: " + agentArgs);
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        for (Class allLoadedClass : allLoadedClasses) {
            System.out.println("agentmain LoadedClass: " + allLoadedClass.getName());
        }
    }
}

两种加载方式区别

image-20230731170000902

1、premain 需要目标程序中载入,是在主线程上运行

2、agentmain 需要在启动一个程序(或线程),即 attach 去监听主线程并对其进行扩展

2、加载Agent启动类

(1)方法一

因为Java Agent的特殊性,需要一些特殊的配置,在 META-INF 目录下创建MANIFEST.MF 文件,并在 MANIFEST.MF 文件中指定Agent的启动类;

Manifest-Version: 1.0
Premain-Class: com.rewind.AppAgent
Archiver-Version: Plexus Archiver
Built-By: rewind
Agent-Class: com.rewind.AppAgent
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_301
Can-Redefine-Classes: true
Can-Retransform-Classes: true

pom.xml 需设置打包为 jar包

<packaging>jar</packaging>

(2)方法二

pom 文件加入以下配置

<packaging>jar</packaging>

<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>com.rewind.AppAgent</Premain-Class>
                        <Agent-Class>com.rewind.AppAgent</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

3、打成 jar 包

执行 maven package

4、加载 premain

(1)目标项目启动加载Agent

运行SpringBoot项目

java -javaagent:D:\code_project\java-agent-demo\target\java-agent-demo-1.0-SNAPSHOT.jar -jar springboot.jar

如果是 idea,将下面这个加到 VM选项即可

-javaagent:D:\code_project\java-agent-demo\target\java-agent-demo-1.0-SNAPSHOT.jar

(2)执行

AppAgent.premain start
agentArgs: null
main 方法执行

5、加载agentmain

(1)目标主程序

无侵入

public static void main(String[] args) throws InterruptedException {
    // get name representing the running Java virtual machine.
    String name = ManagementFactory.getRuntimeMXBean().getName();
    System.out.println(name);
    // get pid
    String pid = name.split("@")[0];
    System.out.println("Pid:" + pid);

    while (true) {
        Thread.sleep(2000);
        System.out.println("Hello World");
    }
}

(2)attach程序

依赖

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8.0</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

main方法

public class AgentAttach {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, InterruptedException {
        // 85355 表示目标进程的PID
        VirtualMachine virtualMachine = VirtualMachine.attach("25068");
        // 指定Java Agent的jar包路径
        try {

            while (true) {
                virtualMachine.loadAgent("D:\\code_project\\java-agent-demo\\target\\java-agent-demo-1.0-SNAPSHOT.jar", "Agent");
                Thread.sleep(3000);
            }

        } catch (AgentLoadException e) {
            e.printStackTrace();
        } catch (AgentInitializationException e) {
            e.printStackTrace();
        }finally {
            virtualMachine.detach();
        }

    }
}

改进,类似 arthas 的切入方式

//获取当前系统所有的虚拟机,类似 jps 命令。
List<VirtualMachineDescriptor> machineDescriptorList = VirtualMachine.list();
HashMap<String, VirtualMachineDescriptor> map = new HashMap<>();
int i = 0;
StringBuilder menu = new StringBuilder("请输入需要连接的虚拟机对应的id\n");
menu.append("id\tpid\tname\n");
for (VirtualMachineDescriptor virtualMachineDescriptor : machineDescriptorList) {
    map.put(Integer.toString(i), virtualMachineDescriptor);
    menu.append(Integer.toString(i)).append("\t")
        .append(virtualMachineDescriptor.id()).append("\t")
        .append(virtualMachineDescriptor.displayName()).append("\n");
    i++;
}
System.out.println(menu);
Scanner scanner = new Scanner(System.in);
String next = scanner.next();
VirtualMachineDescriptor virtualMachineDescriptor = map.get(next);

String id = virtualMachineDescriptor.id();
VirtualMachine virtualMachine = null;
try {
    virtualMachine = VirtualMachine.attach(id);
    // 将当前JVM 链接上目标JVM,并加载 loadAgent jar 包且传递参数。
    virtualMachine.loadAgent("D:\\code_project\\java-agent-demo\\target\\java-agent-demo-1.0-SNAPSHOT.jar", "Agent");

} catch (Exception e) {
    e.printStackTrace();
} finally {
    try {
        if (virtualMachine != null){
            // 卸载虚拟机
            virtualMachine.detach();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

(3)测试

目标程序输出

前面未启动 attach 时,不会执行 agentmain 方法

attach 程序执行后,将会执行 agentmain 方法

Hello World
Hello World
Hello World
Hello World
Hello World
AppAgent.agentmain start
agentArgs: Agent
Hello World
AppAgent.agentmain start
agentArgs: Agent
Hello World
Hello World
AppAgent.agentmain start
agentArgs: Agent
Hello World
AppAgent.agentmain start
agentArgs: Agent

三、Instrumentation API

public interface Instrumentation {

    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,
    //如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
    //对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    //是否允许对class retransform
    boolean isRetransformClassesSupported();

    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    //是否允许对class重新定义
    boolean isRedefineClassesSupported();

    //此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
    //在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
    //该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
    void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;

    //获取已经被JVM加载的class,有className可能重复(可能存在多个classloader)
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();
}

https://www.cnblogs.com/zyl2016/p/13666945.html


  目录