Backend Development 20 min read

Understanding and Implementing Java Agent (Premain and Agentmain) for Bytecode Manipulation

This article introduces Java Agent technology, explains the differences between premain and agentmain modes, demonstrates how to build, package, and attach agents using Maven and the Attach API, and shows practical bytecode manipulation techniques with Instrumentation and Javassist, complete with code examples.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Understanding and Implementing Java Agent (Premain and Agentmain) for Bytecode Manipulation

Java Agent, introduced after JDK 1.5, allows developers to create independent agents that can monitor, modify, or replace classes at the JVM level, similar to AOP but operating on the virtual machine itself. The article first compares the scope, components, and execution modes of AOP and Java Agent, highlighting that agents run at the VM level and require two projects: the agent and the target application.

Premain Mode

In premain mode the agent runs before the main application. A simple agent prints a message and its arguments:

public class MyPreMainAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain start");
        System.out.println("args:" + agentArgs);
    }
}

The agent JAR must contain a Premain-Class entry in its MANIFEST.MF . The Maven maven-jar-plugin can be configured as follows:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>
                    <manifestEntries>
                        <Premain-Class>com.cn.agent.MyPreMainAgent</Premain-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>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

The target application only needs a main method:

public class AgentTest {
    public static void main(String[] args) {
        System.out.println("main project start");
    }
}

Running the application with the agent is done via the -javaagent JVM option, e.g.:

java -javaagent:myAgent.jar -jar AgentTest.jar

Multiple agents can be chained by specifying several -javaagent options. The article also shows that an exception thrown in premain aborts the main program, illustrating a limitation of this mode.

Agentmain Mode

Agentmain runs after the JVM has started and uses the attach mechanism. The agent class looks like:

public class MyAgentMain {
    public static void agentmain(String agentArgs, Instrumentation instrumentation) {
        System.out.println("agent main start");
        System.out.println("args:" + agentArgs);
    }
}

The Maven manifest must contain an Agent-Class entry:

<manifestEntries>
    <Agent-Class>com.cn.agent.MyAgentMain</Agent-Class>
    <Can-Redefine-Classes>true</Can-Redefine-Classes>
    <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>

The target program can be a simple class that blocks on System.in to keep the JVM alive:

public class AgentmainTest {
    public static void main(String[] args) throws IOException {
        System.in.read();
    }
}

Attachment is performed with the com.sun.tools.attach.VirtualMachine API (requires tools.jar as a system dependency):

VirtualMachine vm = VirtualMachine.attach("16392");
vm.loadAgent("F:\\Workspace\\MyAgent\\target\\myAgent-1.0.jar", "param");

After attachment, the agent’s agentmain method executes, demonstrating dynamic loading.

Instrumentation API

The article explains three core methods of Instrumentation :

addTransformer(ClassFileTransformer transformer) – registers a transformer that can modify class bytecode before the class is defined.

redefineClasses(ClassDefinition... definitions) – replaces the bytecode of already loaded classes.

retransformClasses(Class ... classes) – works in agentmain mode to re‑transform already loaded classes.

Examples include a Fruit class whose method is replaced with a different implementation by returning the bytes of a pre‑compiled Fruit2.class file, and a transformer that reads the replacement class file and returns its byte array.

public class FruitTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class
classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (!className.equals("com/cn/hydra/test/Fruit"))
            return classfileBuffer;
        String fileName = "F:\\Workspace\\agent-test\\target\\classes\\com\\cn\\hydra\\test\\Fruit2.class";
        return getClassBytes(fileName);
    }
    public static byte[] getClassBytes(String fileName) {
        File file = new File(fileName);
        try (InputStream is = new FileInputStream(file);
             ByteArrayOutputStream bs = new ByteArrayOutputStream()) {
            long length = file.length();
            byte[] bytes = new byte[(int) length];
            int n;
            while ((n = is.read(bytes)) != -1) {
                bs.write(bytes, 0, n);
            }
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

Using redefineClasses the article shows how to replace Fruit with Fruit2 at runtime, and with retransformClasses it demonstrates live re‑transformation after the program has started.

Javassist Integration

To avoid manual byte array handling, the article introduces Javassist for easier bytecode manipulation. A transformer creates a copy of the original method, renames the original, and inserts timing logic into the copy:

static byte[] calculate() throws Exception {
    ClassPool pool = ClassPool.getDefault();
    CtClass ctClass = pool.get("com.cn.hydra.test.Fruit");
    CtMethod ctMethod = ctClass.getDeclaredMethod("getFruit");
    CtMethod copyMethod = CtNewMethod.copy(ctMethod, ctClass, new ClassMap());
    ctMethod.setName("getFruit$agent");
    StringBuffer body = new StringBuffer("{\n")
        .append("long begin = System.nanoTime();\n")
        .append("getFruit$agent($$);\n")
        .append("System.out.println(\"use \"+(System.nanoTime() - begin) +\" ns\");\n")
        .append("}\n");
    copyMethod.setBody(body.toString());
    ctClass.addMethod(copyMethod);
    return ctClass.toBytecode();
}

The resulting agent prints the execution time of Fruit.getFruit() each time it is called.

Conclusion

Although Java Agent may not be used daily, it powers hot‑deployment, monitoring, and performance‑analysis tools. By mastering premain, agentmain, and the Instrumentation API, developers can extend or modify Java applications at runtime, opening up powerful possibilities for advanced backend development.

backendJavaInstrumentationbytecodemavenagentJavassist
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.