字节码增强技术&手写一个 Java Agent

Java
321
0
0
2023-06-18

1 字节码 增强技术

字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。字节码的实现方式有下图几种:

字节码增强技术&手写一个 Java Agent

1.1 ASM

ASM可以直接生成. Class 字节码文件,也可以在类被加载入 JVM 之前动态修改类行为。 ASM的应用场景有 AOP (Cglib就是基于ASM)、热部署、修改其他 jar 包中的类等。

过程如下

(1)先通过ClassReader读取编译好的. class文件

(2)其通过 访问者模式 (Visitor)对字节码进行修改 常见的 Visitor 类有:

  • MethodVisitor,对方法进行修改
  • FieldVisitor,对变量进行修改的等
  • Annotation Visitor, 访问注解

(3)通过ClassWriter重新构建编译修改后的字节码文件、或者将修改后的字节码文件输出到文件中

字节码增强技术&手写一个 Java Agent

不足 ASM虽然可以达到修改字节码的效果,但是代码实现上更偏底层,是一个个虚拟机指令的集合。

IDEA 下的插件:ASM Bytecode Outline 能够将Java代码转换成ASM中的指令实现.

1.2 JavaAssist

利用 Javassist 实现字节码增强时,能动态改变类的结构或者动态生成类。直接使用java编码的形式,而不需要了解虚拟机指令,编程简单。

JavaAssist中核心的类是ClassPool、CtClass、CtMethod、CtField。

  • ClassPool:保存CtClass的Map,通过classPool.get(类全路径名)来获取CtClass
  • CtClass:编译时类信息,它是一个class文件在代码中的抽象表现形式
  • CtMethod:对应类中的方法
  • CtField:对应类中的属性、变量

不足

ASM和JavaAssist 只能在类加载前对类中字节码进行修改,不能对运行中的 JVM字节码文件修改加载。

例子

 package com.artemis.xm.agent;

public class A {
    public  void   method(){
        System.out.println("method...");
    }
} 
 public class JavassistTest {
    public  static  void main(String[] args) throws NotFound Exception , CannotCompileException, IllegalAccessException, InstantiationException, IOException {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("com.artemis.xm.agent.A");
        CtMethod cm = cc.getDeclaredMethod("method");
        cm.insertBefore("{ System.out.println("start"); }");
        cm.insertAfter("{ System.out.println("end"); }");
        Class c = cc.toClass();
        A a = (A) c.newInstance();
        a.method();
    }
}

输出

 start
method...
end 

1.3 instrument

Java 从 1.5 开始提供了 java.lang.instrument,为Java 程序提供 API,比如用于监控、收集性能信息、诊断问题等。

Instrumentation 是 java.lang.instrument 包下的一个接口,这个接口允许我们对已加载和未加载的类进行修改。

Instrumentation接口定义

 public interface Instrumentation {
     //为Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

       //对JVM已经加载的类重新触发类加载
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    //获取当前 JVM 加载的所有类对象
    Class[] getAllLoadedClasses();
} 

调用Instrumentation#addTransformer设置 transformer以后,后续JVM加载所有类之前都会被这个transform方法拦截。

ClassFileTransformer接口定义

 
public interface ClassFileTransformer {
   public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
        // 在这里读取、转换类文件
        return classBytes;
    }
} 

ClassFileTransformer接口的transform方法会在类文件被加载时调用,transform方法接收原类文件的字节数组,而在方法里,可以利用ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。

2 Java Agent

2.1 Java Agent 简介

Java Agent 使用 Instrumentation 接口(java.lang.instrument)来编写 Agent,Instrumentation 的 API 用来读取和改写当前 JVM 的类。

Java Agent 是⼀个特殊的 Jar 包,它并不能单独启动的,而必须依附于一个 JVM 进程。

Java Agent 有premain 和 agentmain 两种:

premain代理:这个代理类包含一个 premain 方法。JVM 在类加载时候会先执行代理类的 premain 方法,再执行 Java 程序本身的 main 方法,这就是 premain 名字的来源。在 premain 方法中可以对加载前的 class 文件进行修改。

agentmain代理:在JVM 启动后通过JVMTI的Attach API机制 远程加载。通过Attach API,我们可以访问已经启动的 Java 进程,从而拦截类的加载。从 JDK 1.6 开始Instrumentation支持了在运行时对类定义的修改。

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

2.2 实现一个Java Agent

使用Java Agent需要几个步骤:

  1. 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
  2. 创建一个Premain-Class 指定的类,类中包含 premain 方法
  3. 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
  4. 使用参数 -javaagent: jar包路径 启动要代理的方法。

2.2.1 定义需要增强的类的增强方法

定义需要增强的类Cat

 public class Cat {
    public void beginSleep() {
        while (true) {
            sleep();
            try {
                Thread.sleep(L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void sleep() {
        System.out.println("cat is sleeping");
    }
} 

preMain

 public class PreMainTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!className.equals("com.dc.husky.agent.Cat")) {
            return null;
        }
        System.out.println("premain transform Class:" + className);
        return classfileBuffer;
    }
} 

agentMain

 public class AgentMainTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        System.out.println("agentMain transform Class:" + className);
        try {
            ClassPool cp = ClassPool.getDefault();
            ClassClassPath classPath = new ClassClassPath(this.getClass());
            cp.insertClassPath(classPath);
            CtClass cc = cp.get("com.dc.husky.agent.Cat");
            CtMethod m = cc.getDeclaredMethod("sleep");
            m.insertBefore("{ System.out.println("start"); }");
            m.insertAfter("{ System.out.println("end"); }");
            return cc.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
} 

定义Agent

 public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new PreMainTransformer(), true);
        System.out.println("premain agent loaded !");
    }

    public static void agentmain(String args, Instrumentation inst) {
        inst.addTransformer(new AgentMainTransformer(), true);
        try {
            //重定义类并载入新的字节码
            inst.retransformClasses(Cat.class);
            System.out.println("Agent Load Done.");
        } catch (Exception e) {
            System.out.println("agent load failed!");
        }
    }
} 

2.2.2 配置

在 resources 目录下新建目录:META-INF,在该目录下新建MANIFREST.MF文件

MANIFREST.MF文件的作用

  • Premain-Class :包含 premain 方法的类(类的全路径名)
  • Agent-Class :包含 agentmain 方法的类(类的全路径名)
  • Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
  • Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)

2.2.3 打包agent jar包

maven pom加入下列plugin,打包agent jar包

 <build>
    <plugins>
        <plugin>
         ...
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestFile>
                        src/main/resources/META-INF/MANIFEST.MF
                    </manifestFile>
                </archive>
            </configuration>

            <executions>
                <execution>
                    <goals>
                        <goal>attached</goal>
                    </goals>
                    <phase>package</phase>
                </execution>
            </executions>
        </plugin>

    </plugins>
</build> 

2.2.4 目标 JVM 进程

启动目标进程,得到当前的Pid

 public static void main(String[] args) {
    String name = ManagementFactory.getRuntimeMXBean().getName();
    String s = name.split("@")[];
    //打印当前Pid
    System.out.println("pid:"+s);
    Cat cat = new Cat();
    cat.beginSleep();
} 

2.2.5 Attacher JVM 进程

新启动一个Attacher进程,配置下列vm参数

 -javaagent:/Users/**/IdeaProjects/huskey/huskey-agent/target/huskey-agent-.0.0-SNAPSHOT-jar-with-dependencies.jar 

Attacher进程代码

 public class Attacher {
    public static void main(String[] args) {
        try {
            String pid = "";
            VirtualMachine virtualMachine = VirtualMachine.attach(pid);
            virtualMachine.loadAgent("/Users/qian/IdeaProjects/huskey/huskey-agent/target/huskey-agent-.0.0-SNAPSHOT-jar-with-dependencies.jar");
            Thread.sleep();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

2.3 效果

以下为运行时重新载入类的效果: 先运行目标 JVM 进程,得到pid,可以在控制台看到每隔五秒输出一次”cat is sleeping”。接着启动Attacher中的main()方法,并将目标JVM 的pid传入。此时回到目标JVM的控制台,可以看到现在每隔五秒输出”cat is sleeping”前后会分别输出”start”和”end”,也就是说完成了运行时的字节码增强,并重新载入了这个类。