跳转至

Premain模式开发

基本概念

Premain模式是JavaAgent最常用的加载方式,在目标应用的主方法执行之前完成Agent的加载和初始化。通过在JVM启动参数中指定 -javaagent 参数,可以将Agent字节码注入到JVM中。

开发步骤

1. 创建Maven项目结构

agent-project/
├── pom.xml
└── src/main/java/
    └── com/example/
        └── MyAgent.java

2. 定义Agent类

package com.example;

import java.lang.instrument.Instrumentation;

public class MyAgent {
    public static void premain(String args, Instrumentation inst) {
        System.out.println("[MyAgent] premain executed with args: " + args);
        inst.addTransformer(new MyTransformer());
    }
}

3. 创建类转换器

package com.example;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
            ClassLoader loader,
            String className,
            Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("[Transformer] Loading class: " + className);
        return null;
    }
}

完整示例:方法耗时统计

目标应用

public class TargetApp {
    public void doSomething() {
        System.out.println("doing something...");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TargetApp app = new TargetApp();
        for (int i = 0; i < 5; i++) {
            app.doSomething();
        }
    }
}

Agent实现

import java.lang.instrument.Instrumentation;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import javassist.*;

public class TimingAgent {
    public static void premain(String args, Instrumentation inst) {
        System.out.println("[TimingAgent] Premain started");
        inst.addTransformer(new TimingTransformer());
    }

    static class TimingTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(
                ClassLoader loader,
                String className,
                Class<?> classBeingRedefined,
                ProtectionDomain protectionDomain,
                byte[] classfileBuffer) {
            if (!className.equals("TargetApp")) {
                return null;
            }

            try {
                ClassPool pool = ClassPool.getDefault();
                CtClass clazz = pool.get("TargetApp");

                for (CtMethod method : clazz.getDeclaredMethods()) {
                    method.addLocalVariable("startTime", CtClass.longType);
                    method.insertBefore("startTime = System.nanoTime();");
                    method.insertAfter(
                        "System.out.println(\"[Timing] " +
                        method.getName() + " cost: \" + " +
                        "(System.nanoTime() - startTime) / 1000000 + \"ms\");"
                    );
                }

                return clazz.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
}

Maven配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>timing-agent</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.29.2-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.TimingAgent</mainClass>
                            <addClasspath>true</addClasspath>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

MANIFEST配置

如果使用普通Java项目(非Maven),需要手动创建 MANIFEST.MF:

Manifest-Version: 1.0
Premain-Class: com.example.TimingAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

打包时确保 MANIFEST.MF 在 JAR 的 META-INF 目录下:

jar cfm timing-agent.jar META-INF/MANIFEST.MF com/example/*.class

运行Agent

打包Agent

mvn clean package

运行目标应用

java -javaagent:timing-agent.jar -jar target-app.jar

输出示例

[TimingAgent] Premain started
doing something...
[Timing] doSomething cost: 102ms
doing something...
[Timing] doSomething cost: 101ms
...

注意事项

注意事项 说明
类名转换 返回 null 表示不修改类
异常处理 transform方法异常会导致类加载失败
类加载器 注意要注入类的类加载器上下文
性能影响 Agent本身会引入一定的性能开销
线程安全 多线程环境下注意并发安全

进阶技巧

1. 过滤目标类

if (!className.startsWith("com/example/")) {
    return null;
}

2. 获取方法参数名

CtMethod method = ...;
MethodInfo info = method.getMethodInfo();
LocalVariableAttribute attr = (LocalVariableAttribute)
    info.getCodeAttribute().getAttribute(LocalVariableAttribute.tag);

3. 注解驱动增强

@interface Timed {
}

if (method.hasAnnotation("com.example.Timed")) {
    // 只增强带注解的方法
}