意见箱
恒创运营部门将仔细参阅您的意见和建议,必要时将通过预留邮箱与您保持联络。感谢您的支持!
意见/建议
提交建议

java - Java Agent 技术与原理_个人文章

来源:恒创科技 编辑:恒创科技编辑部
2024-01-30 13:46:59
Instrumentation

Instrumentation是JDK 1.5的一个新特性,通过java.lang.instrument包可以实现一个独立于应用程序的Agent程序,能够替换和修改类的定义。有了这样的功能,开发者可以实现灵活的运行时虚拟机监控和Java类增强,实际上提供了一种虚拟机级别的AOP实现方式。

Java Agent Demo

下面介绍通过java.lang.instrument编写Agent的一般方法。

实现Agent启动方法

Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:


java - Java Agent 技术与原理_个人文章

[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。

如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:

[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);

这两组方法的第一个参数agentArgs是随同–javaagent一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。instrumentationInstrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。

public class Agent {

    public static void premain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("Agent premain start ...");
        System.out.println("Agent args: " + agentArgs);
        instrumentation.addTransformer(new AppInitTramsformer());
    }

    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("Agent agentmain start ...");
        System.out.println("Agent args: " + agentArgs);
        instrumentation.addTransformer(new AppInitTramsformer());
    }
}
实现Transformer

Instrumentation接口中,通过addTransformer方法来增加一个类转换器,类转换器由类ClassFileTransformer接口实现。ClassFileTransformer接口中唯一的方法transform用于实现类转换,当类被加载的时候,就会调用transform方法,进行类转换。在运行时,我们可以通过InstrumentationredefineClasses方法进行类重定义。实现时注意不要增加、删除或者重命名字段和方法,改变方法的签名或者类的继承关系。

修改字节码可以借助ASMjavassistbytebuddy等工具。下例中使用了javassist

public class AppInitTramsformer implements ClassFileTransformer {

    private static final String INJECTED_CLASS = "com.github.godshang.agent.Agent";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        String realClassName = className.replace("/", ".");
        if (realClassName.equals(INJECTED_CLASS)) {
            System.out.println("拦截到的类名:" + realClassName);
            CtClass ctClass;
            try {
                ClassPool classPool = ClassPool.getDefault();
                ctClass = classPool.get(realClassName);

                CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
                for (CtMethod method : declaredMethods) {
                    System.out.println(method.getName() + "方法被拦截");
                    method.addLocalVariable("time", CtClass.longType);
                    method.insertBefore("System.out.println(\"---开始执行---\");");
                    method.insertBefore("time = System.currentTimeMillis();");
                    method.insertAfter("System.out.println(\"---结束执行---\");");
                    method.insertAfter("System.out.println(\"运行耗时: \" + (System.currentTimeMillis() - time));");
                }
                return ctClass.toBytecode();
            } catch (Throwable e) {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}
MANIFEST.MF 文件

编写的Agent如何被外部应用程序知晓呢?依靠的是MANIFEST.MF文件。文件的具体路径是:src/main/resources/META-INF/MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: com.github.godshang.agent.Agent
Agent-Class: com.github.godshang.agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

MANIFEST.MF文件Premain-Class对应premain入口即启动时加载Agent,Agent-Class对应agentmain入口即运行时加载Agent。Can-Redefine-Classes表示是否能重定义此代理所需的类,默认值是false。Can-Retransform-Classes表示是否能重转换此代理所需的类,默认值是false。

除了直接创建MANIFEST.MF文件,也可以在Maven中配置编译打包插件。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <!--自动添加META-INF/MANIFEST.MF -->
            <manifest>
                <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
                <Menifest-Version>1.0</Menifest-Version>
                <Premain-Class>com.github.godshang.agent.Agent</Premain-Class>
                <Agent-Class>com.github.godshang.agent.Agent</Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>
加载Agent

如果是启动时进行静态加载,需要在启动参数中增加-javaagent参数:

public class AppMain {

    /**
     * -javaagent:E:\github\java-agent\agent\target\agent-1.0-SNAPSHOT.jar=hello
     * @param args
     */
    public static void main(String[] args) {
        AppInit appInit = new AppInit();
        appInit.init();
    }
}

如果是运行时进行动态加载,可参考如下代码:

public class AttachMain {

    public static void main(String[] args) {
        for (VirtualMachineDescriptor vmd : VirtualMachine.list()) {
            System.out.println(vmd.displayName());
            if (vmd.displayName().endsWith("AttachMain")) {
                try {
                    VirtualMachine vm = VirtualMachine.attach(vmd.id());
                    vm.loadAgent("E:\\github\\java-agent\\agent\\target\\agent-1.0-SNAPSHOT.jar=hello");
                    vm.detach();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        AppInit appInit = new AppInit();
        appInit.init();
    }
}
Java Agent 原理

Instrumentation的底层实现 依赖于JVMTI,也就是JVM Tool Interface

JVMTI

JVMTI(JVM Tool Interface)是Java虚拟计对外提供的Native编程接口,是JVM暴露出来给用户扩展使用的接口集合。通过JVMTI外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。JVMTI是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展实现自己的逻辑。

JVMTI Agent

实现了JVMTI的客户端程序称之为agent,它其实就是利用JVMTI暴露出来的接口实现用户自行的逻辑。在JVMTI Agent中主要有三个方法:

Agent_OnLoad方法,如果agent在启动时加载,就执行这个方法Agent_OnAttach方法,如果agent不是在启动的时候加载的,是我们先attach到目标线程上,然后对对应的目标进程发送load命令来加载agent,在加载过程中调用Agent_OnAttach函数Agent_OnUnload方法,在agent做卸载掉时候调用

如何使用C++开发Agent,可以参考这篇文章。

Instrument

JVMTI是一套Native接口,在Java SE 5之前,要实现一个Agent只能通过编写Native代码来实现。从Java SE 5开始,可以使用Java的Instrumentation接口来编写Agent。无论是通过Native的方式还是通过Java Instrumentation接口的方式来编写Agent,它们的工作都是借助JVMTI来进行完成。

JPLISAgent全名是Java Programming Language Instrumentation Services Agent,它是一种特殊的JVMTI Agent,作用是初始化所有通过Instrumentation接口编写的Agent,并且也承担着通过JVMTI实现Instrumentation中暴露API的责任。

Referencehttps://docs.oracle.com/javas...https://cloud.tencent.com/dev...https://xz.aliyun.com/t/10186https://juejin.cn/post/708602...https://jueee.github.io/2020/...https://www.overops.com/blog/...
上一篇: java - HashMap和HashSet的区别_个人文章 下一篇: 手机怎么远程登录云服务器?