关于java:Java-Agent-技术与原理

42次阅读

共计 5382 个字符,预计需要花费 14 分钟才能阅读完成。

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,那么能够抉择实现上面的办法:

[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 的责任。

Reference

  1. https://docs.oracle.com/javas…
  2. https://cloud.tencent.com/dev…
  3. https://xz.aliyun.com/t/10186
  4. https://juejin.cn/post/708602…
  5. https://jueee.github.io/2020/…
  6. https://www.overops.com/blog/…

正文完
 0