Java-Agent 介绍

97 阅读5分钟

Java Agent


Java Agent = bytecode instrumentation

Java Agent       一个可以修改字节码的机会(需要熟悉 `java.lang.instrument` 相关的 API)
------------------
Java ASM         操作字节码的类库(常用的字节码的类库有:ASM、ByteBuddy 和 Javassist)
------------------
ClassFile        理论基础

Instrumentation can be inserted in one of three ways :

image.png

java.lang.instrument

  • instrumentation 可以加载修改类中字节码的方法
  • agent jar 的功能实际上是借助于 Instrumentation 对象完成的

image.png

规范定义

  • manifest 中 定义 Premain-Class Agent-Class 属性

  • 根据 定义的 class 执行对应的 premain 和 agentmain 方法

  • 执行时机

    • premain 是在 JVM 启动时执行的(主要在启动时的代理)。
    • agentmain 是在动态附加时执行的(即运行时通过 attach 动态附加的代理)。
  • Manifest =指定=> Agent Class =获取=> Instrumentation =修改字节码=> ClassFileTransformer

image.png

Agent Jar

Java Agent 对应的 .jar 文件里,有三个主要组成部分:

  1. Manifest
    • META-INF/MANIFEST.MF
      • Premain-Class
        • JVM 加载时,调用Premain-Class 属性中指定的 premain 方法
      • Agent-Class
        • JVM 启动之后,通过 Attach API 动态附加 Java Agent 时执行的。这个方法通常用于代理在 JVM 启动后动态附加到运行中的 JVM 实例
      • Can-Redefine-Classes
        • class 是否可被 agent 重新定义;可选,默认为 false
      • Can-Retransform-Classes:
        • class 是否可被 agent 重新转换;可选,默认为 false
      • Can-Set-Native-Method-Prefix
        • 是否可以设置本地方法前缀;可选,默认为 false
      • Boot-Class-Path
        • 指定由引导类 bootstrap class loader 加载器加载的类路径
      • Launcher-Agent-Class
        • 在应用程序的 main 方法执行之前插入代理代码,通常用于做一些应用级的初始化工作。
  2. Agent Class
    • LoadTimeAgent.class: premain
    • DynamicAgent.class: agentmain
  3. ClassFileTransformer
    • ASMTransformer.class

LoadTimeAgent VS DynamicAgent

image.png

  • 涉及虚拟机数量

    • Load-Time Instrumentation 只涉及到一个虚拟机
    • Dynamic Instrumentation 涉及到两个虚拟机
  • 执行时机

    • Load-Time Instrumentation,会执行 Agent Jar 当中的 premain() 方法;premain() 方法是先于 main() 方法执行,此时 Application 当中使用的大多数类还没有被加载

    • Dynamic Instrumentation,会执行 Agent Jar 当中的 agentmain() 方法;而 agentmain() 方法是往往是在 main() 方法之后执行,此时 Application 当中使用的大多数类已经被加载

  • 能力

    • Load-Time Instrumentation 可以做很多事情:添加和删除字段、添加和删除方法等。
    • Dynamic Instrumentation 做的事情比较有限,大多集中在对于方法体的修改。
  • 线程不同

    • Load-Time Instrumentation 是运行在 main 线程
    • Dynamic Instrumentation 是运行在 Attach Listener 线程
  • Exception 处理

    • Load-Time Instrumentation 时,出现异常,会报告错误信息,并且停止执行,退出虚拟机。
    • Dynamic Instrumentation 时,出现异常,会报告错误信息,但是不会停止虚拟机,而是继续执行。

命令行写法

image.png

  • 定义 option-text 格式,并解析为 key-value
    username:tomcat,password:123456
    username=tomcat,password=123456
    

类介绍

ClassDefinition

  • 封装了 Class 和 byte[] 两个字段
  • mClass 位于堆区,关联了 方法区的 mClassFile 字节数组
  • 当想要 重新 定义某个类时,可以根据 ClassDefinition 找到方法区的字节数组
package java.lang.instrument;

public final class ClassDefinition {
    private final Class<?> mClass;
    private final byte[] mClassFile;
}

ClassFileTransformer

An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM.

public interface ClassFileTransformer {
    byte[] transform(ClassLoader         loader,
                     String              className,
                     Class<?>            classBeingRedefined,
                     ProtectionDomain    protectionDomain,
                     byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}
  • ClassLoader loader:加载当前类的类加载器, 如果值为 null,则表示 bootstrap loader
  • String className:internal class name(java/lang/String)当前类的名字,也是你要修改的类名称。
  • Class<?> classBeingRedefined:如果该类正在被重新定义(例如在热部署中),这个参数会包含正在被重新定义的类的信息。如果是新加载的类,这个参数为 null
  • ProtectionDomain protectionDomain:类的保护域,表示该类的安全限制,比如权限控制等。
  • byte[] classfileBuffer:表示原始的字节码数据,也是你需要修改的字节数组。
  • byte[] :返回修改后的字节码。如果不进行任何修改,可以直接返回原始的字节码 或者 null。如果进行了修改,返回一个新的字节码数组,这将作为最终加载到JVM中的字节码。

Instrumentation

public interface Instrumentation {

    // 读取 `META-INF/MANIFEST.MF` 文件中的属性信息
    boolean isRedefineClassesSupported();
    boolean isRetransformClassesSupported();
    boolean isNativeMethodPrefixSupported();
    
    
    // 添加和删除 `ClassFileTransformer`:
    void addTransformer(ClassFileTransformer transformer);
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    boolean removeTransformer(ClassFileTransformer transformer);
    
    
    // 重新定义Class字节码
    void redefineClasses(ClassDefinition... definitions)
            throws ClassNotFoundException, UnmodifiableClassException;
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    
    // 获取内存中已经加载的类,已经初始化的类
    Class[] getAllLoadedClasses();
    Class[] getInitiatedClasses(ClassLoader loader);
    boolean isModifiableClass(Class<?> theClass);
    
    // 获取对象大小
    long getObjectSize(Object objectToSize);
    
    
    // native 也可通过 Instrumentation 修改native方法
    boolean isNativeMethodPrefixSupported();
    void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);


    // java 9 引入的
    boolean isModifiableModule(Module module);
    void redefineModule (Module module,
                         Set<Module> extraReads,
                         Map<String, Set<Module>> extraExports,
                         Map<String, Set<Module>> extraOpens,
                         Set<Class<?>> extraUses,
                         Map<Class<?>, List<Class<?>>> extraProvides);
}

redefine and retransform

There are two kinds of transformers, determined by the canRetransform parameter of Instrumentation.addTransformer(ClassFileTransformer,boolean):

  • retransformation capable transformers that were added with canRetransform as true
  • retransformation incapable transformers that were added with canRetransform as false or where added with Instrumentation.addTransformer(ClassFileTransformer)

image.png

  • 两者都是对已经加载的类(already loaded classes)进行修改。
  • 处理方式不同:替换(redefine)和修改(retransform)
  • 方法的调用时机有 3 种
    1. 类加载的时候
    2. 调用 Instrumentation.redefineClasses 方法的时候
    3. 调用 Instrumentation.retransformClasses 方法的时候
  • 对于正在加载的类进行修改,属于 define。
  • 对于已经加载的类进行修改,它属于 redefine 和 retransform 的范围。

对于已经加载的类(loaded class),redefine 侧重于以“新”换“旧”,而 retransform 侧重于对“旧”的事物进行“修补”。

 defineredefineretransform
Interface AddOKNONO
Field AddOKNONO
Method AddOKNONO
Method RemoveOKNONO
Method Body ModifyOKOKOK
  • load,是类在加载的过程当中,JVM 内部机制来自动触发。
  • redefine 和 retransform,是我们自己写代码触发。

手动打包

LoadTimeAgent

  • 编译

    • javac -d out -sourcepath src $(find src -name "*.java")
  • 将 manifest 文件添加到编译路径

    • cp src/main/manifest.txt out
  • 打包

    • jar -cvfm TestAgent.jar out/manifest.txt -C out com/dawn/agent
      jar cvfm TestAgent.jar:创建一个 JAR 文件并指定输出文件名 TestAgent.jar。c 是创建 JAR 文件,f 是指定文件名, m 是 mainfest 文件
      
      -C out:表示切换到 out 目录,在该目录下执行后续的打包操作。-C 选项是 jar 命令的一个选项,用来改变工作目录。
      
      com/dawn/agent:表示只打包 com.dawn.agent 包以及其子包中的 .class 文件
      

    image.png

    打印加载的类信息 image.png

Dynamic Agent

借助 JDK 内置的 ASM 打印出方法接收的参数,使用 Dynamic Instrumentation 的方式实现。

manifest.txt

Agent-Class: com.dawn.agent.DynamicAgent
Can-Retransform-Classes: true

VMAttach.java

VMAttach 获取 VM 列表,根据条件 attach 到对应进程

package com.dawn.agent.attach;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class VMAttach {

    /**
     * 需要 tools.jar
     */
    public static void main(String[] args) throws Exception {
        String agent = "TestAgent.jar";
        System.out.println("Agent Path: " + agent);
        List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : vmds) {
            if (vmd.displayName().equals("com.dawn.application.Program")) {
                VirtualMachine vm = VirtualMachine.attach(vmd.id());
                System.out.println("Load Agent");
                vm.loadAgent(agent);
                System.out.println("Detach");
                vm.detach();
            }
        }
    }
}

DynamicAgent.java

package com.dawn.agent;

import com.dawn.agent.instrument.ASMTransformer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class DynamicAgent {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("Agent-Class: " + DynamicAgent.class.getName());
        ClassFileTransformer transformer = new ASMTransformer();
        try {
            inst.addTransformer(transformer, true);
            Class<?> targetClass = Class.forName("com.dawn.application.HelloWorld");
            inst.retransformClasses(targetClass);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        finally {
            inst.removeTransformer(transformer);
        }
    }

}

编译:

  • VMAttach:编译需要tools.jar

    • javac -XDignore.symbol.file -encoding UTF-8 -cp "${JAVA_HOME}\lib\tools.jar;." -d out -sourcepath src $(find src -name "*.java")
  • 将 manifest 文件添加到编译路径

    • cp src/main/manifest.txt out

打包:

  • $ jar -cvfm TestAgent.jar out/manifest.txt -C out com/dawn/agent

运行:

  1. 运行Program $ java -cp out com.dawn.application.Program

  2. 运行VMAttach $ java -cp "C:\soft\Programs\java\jdk1.8.0_441/lib/tools.jar;out" com.dawn.agent.attach.VMAttach

  3. 运行结果 image.png

Maven 打包

使用插件

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.4</version>
    <configuration>
        <minimizeJar>true</minimizeJar>
        <filters>
            <filter>
                <artifact>*:*</artifact>
                <excludes>
                    <exclude>application/*</exclude>
                    <exclude>run/*</exclude>
                </excludes>
            </filter>
        </filters>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer
                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <manifestEntries>
                            <Main-Class>com.dawn.application.HelloProgram</Main-Class>
                            <Premain-Class>com.dawn.agent.LoadTimeAgent</Premain-Class>
                            <Agent-Class>com.dawn.agent.DynamicAgent</Agent-Class>
                            <Launcher-Agent-Class>com.dawn.agent.LauncherAgent</Launcher-Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>