agentmain | JVM运行时的代码增强

2022-06-27 15:06:48 浏览数 (1)

在前文中,介绍了Instrumentation中的premain功能, 这次再一起看下它的agentmain功能.

一. agentmain简介

premain不同的是, agentmain是JVM 利用attach机制,在运行时添加动态代理的方式, 完成字节码的修改.

1.1

agentmain函数

agentmain函数与premain类似, 也有两种写法; 其他参数以及方法优先级也是与premain类似的.

代码语言:javascript复制
// 方式1
public static void agentmain(String agentArgs, Instrumentation inst);
// 方式2
public static void agentmain(String agentArgs);

1.2

MANIFEST.MF

与premain类似,打jar包时,也需要一个元文件; 元数据文件目录(maven工程):

代码语言:javascript复制
/src/main/resources/META-INF/MANIFEST.MF

文件内容:

代码语言:javascript复制
Manifest-Version: 1.0
Agent-Class: com.instrument.agentmain.MyAgentmain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

1.3

jar包生成

打jar包方式以及操作都与premain类似, 这里就不赘述了.

二. Hello World

一起看个示例, 了解下agentmain是如何在运行时, 动态增强的. 我们的目标是在不重启的情况下, 打印出abc()方法的运行时间.

2.1

Agentmain类

agentmain类中的参数会传入类全名和方法名, 并通过TimingTransformer类增加方法功能

代码语言:javascript复制
package com.instrument.agentmain;

import java.lang.instrument.Instrumentation;

public class MyAgentmain {

    public static void agentmain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println("Hi, I'm agentmain agent! 2:"   agentArgs);
        Class[] classes = inst.getAllLoadedClasses();
        System.out.println("length:" classes.length);
        String cname=agentArgs.split(",")[0];
        Class<?> targetClass = Thread.currentThread().getContextClassLoader().loadClass(cname);
        String methodName=agentArgs.split(",")[1];
        inst.addTransformer(new TimingTransformer(cname,methodName),true);
        inst.retransformClasses(targetClass);
    }

}

2.2

TimingTransformer类

TimingTransformer是解析对应的类和方法, 并利用javassist对原方法进行改造增加, 添加时间统计逻辑.

代码语言:javascript复制
package com.instrument.agentmain;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

public class TimingTransformer implements ClassFileTransformer {

    private String cname;
    private String methodName;

    public TimingTransformer(String cname, String methodName) {
        this.cname = cname;
        this.methodName = methodName;
        System.out.println("method in timing:"   cname   ","   methodName);
    }

    public byte[] transform(ClassLoader loader, String className, Class cBR,
            java.security.ProtectionDomain pD, byte[] classfileBuffer) {
        try {
            System.out.println("className:"   className);
            if (!className.equals(cname)) {
                return null;
            }
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get(cname);
            CtMethod[] methods = cc.getMethods();
            for (int i = 0; i < methods.length; i  ) {
                CtMethod method = methods[i];
                if (method.getName().equals(methodName)) {
                    System.out.println("->className:"   className   ",method:"   method.getName());
                    // add time
                    addTimer(method);
                }
            }
            return cc.toBytecode();
        } catch (NotFoundException | CannotCompileException | IOException e) {
            e.printStackTrace();
            System.exit(0);
        }
        return null; // No transformation required
    }

    private void addTimer(CtMethod method) throws CannotCompileException {
        method.addLocalVariable("startTime", CtClass.longType);
        method.insertBefore("long startTime = System.nanoTime();");
        method.insertAfter("System.out.println("time:"   (System.nanoTime() - startTime));");
//        long startTime = System.nanoTime();
//        System.out.println("time:"   (System.nanoTime() - startTime));
    }

}

2.3

pom依赖

引入pom依赖, 再对agentmain打成jar包, 我们的hack类就准备完毕了.

代码语言:javascript复制
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>

2.4

目标类

TestAgentmain是我们要在运行中动态代理的目标类, 需要注意的是程序本身不需要javassist.jar, 但为了配合agentmain使用也是需要引入该pom依赖的.

程序启动运行后, 会打印出进程PID, 后面的VM动态增强时,使用.

代码语言:javascript复制
import java.lang.management.ManagementFactory;

public class TestAgentmain {

    public static void main(String[] args) {
        // 进程PID信息
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(name);
        while (true) {
            new Thread(new WaitThread()).start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
 class WaitThread implements Runnable {

    @Override
    public void run() {
        abc();
    }

    public void abc() {
        System.out.println("abc in method");
    }
}

2.4

TestAgentmain执行结果之一

执行结果, 其中64250为该程序的进程ID. 程序每隔1s会打印日志语句.

代码语言:javascript复制
64250@MacBook-Pro.local
abc in method
abc in method
abc in method
abc in method
abc in method

2.5

添加VM attach接口

这里需要用到上面TestAgentmain运行的进程ID:64250;

同时制定agentmin运行的参数"WaitThread,abc", 代码要修改WaitThread类中的abc()方法.

代码语言:javascript复制
public class VirtualMachineTest {

    public static void main(String[] args) throws AttachNotSupportedException, IOException,
            AgentLoadException, AgentInitializationException, InterruptedException {
        // attach to target VM PID
        VirtualMachine vm = VirtualMachine.attach("64250");
        vm.loadAgent("/xx/instrument/agent/target/agent-1.0-SNAPSHOT.jar", "WaitThread,abc");
        Thread.sleep(1000);
        System.out.println("attach end");
        vm.detach();
    }
}

2.6

TestAgentmain执行结果之二

再次观察TestAgentmain执行结果, 可以发现能够输出方法的执行时间了. 说明程序已经被动态增强了.

代码语言:javascript复制
abc in method
abc in method
Hi, I'm agentmain agent! 2:WaitThread,abc
length:729
method in timing:WaitThread,abc
className:WaitThread
->className:WaitThread,method:abc
abc in method
time:201286
abc in method
time:129545
abc in method
time:162443

2.7

代码结构

小结

premain和agentmain两种方式最终的目的都是为了回调Instrumentation实例并激活sun.instrument.InstrumentationImpl#transform()(InstrumentationImpl是Instrumentation的实现类)从而回调注册到Instrumentation中的ClassFileTransformer实现字节码修改, 本质功能上没有很大区别. 两者的非本质功能的区别如下:

1. premain需要通过命令行使用外部代理jar包, 即-javaagent:代理jar包路径;agentmain则可以通过attach机制直接附着到目标VM中加载代理, 也就是使用agentmain方式下, 操作attach的程序和被代理的程序可以是完全不同的两个程序.

2. premain方式回调到ClassFileTransformer中的类是虚拟机加载的所有类, 这个是由于代理加载的顺序比较靠前决定的, 在开发者逻辑看来就是: 所有类首次加载并且进入程序main()方法之前, premain方法会被激活, 然后所有被加载的类都会执行ClassFileTransformer列表中的回调.

3. agentmain方式由于是采用attach机制, 被代理的目标程序VM有可能很早之前已经启动, 当然其所有类已经被加载完成, 这个时候需要借助Instrumentation#retransformClasses(Class<?>...classes)让对应的类可以重新转换, 从而激活重新转换的类执行ClassFileTransformer列表中的回调.

4. 通过premain方式的代理Jar包进行了更新的话, 需要重启服务器, 而agentmain方式的Jar包如果进行了更新的话, 需要重新attach, 但是agentmain重新attach还会导致重复的字节码插入问题, 不过也有Hotswap和DCE VM方式来避免.

0 人点赞