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.
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.jarMultiple 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.
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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.