Java Agent Development in Java

Java Agents allow you to intercept and transform classes as they're loaded by the JVM. They provide powerful capabilities for monitoring, profiling, and modifying application behavior at runtime.

1. Java Agent Fundamentals

What is a Java Agent?

  • A Java program that runs before the main application
  • Can modify bytecode of classes as they're loaded
  • Access to JVM instrumentation capabilities
  • Used for profiling, monitoring, logging, security, etc.

Key Components

  • Premain - Agent started with JVM launch
  • Agentmain - Agent attached to running JVM
  • ClassFileTransformer - Transforms class bytes
  • Instrumentation - JVM instrumentation interface

2. Basic Java Agent Structure

Simple Agent with Premain

import java.lang.instrument.Instrumentation;
/**
* Basic Java Agent that prints a message when loaded
*/
public class SimpleAgent {
/**
* Premain method - called when agent is specified at JVM launch
* @param agentArgs Agent arguments from command line
* @param inst Instrumentation object provided by JVM
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== Simple Agent Started ===");
System.out.println("Agent Args: " + agentArgs);
System.out.println("Instrumentation: " + inst);
System.out.println("Class Redefinition Supported: " + inst.isRedefineClassesSupported());
System.out.println("Class Retransformation Supported: " + inst.isRetransformClassesSupported());
System.out.println("Native Method Prefix Supported: " + inst.isNativeMethodPrefixSupported());
}
/**
* Alternative premain signature (without Instrumentation)
*/
public static void premain(String agentArgs) {
System.out.println("=== Simple Agent Started (no Instrumentation) ===");
System.out.println("Agent Args: " + agentArgs);
}
}

MANIFEST.MF for Basic Agent

Manifest-Version: 1.0
Premain-Class: SimpleAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Build-Jdk: 11

Building with Maven

<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

Building with Gradle

jar {
manifest {
attributes(
'Premain-Class': 'SimpleAgent',
'Can-Redefine-Classes': 'true',
'Can-Retransform-Classes': 'true',
'Can-Set-Native-Method-Prefix': 'true'
)
}
}

3. Class Transformation

Method Tracing Agent

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
/**
* Agent that adds method execution tracing to all methods
*/
public class MethodTracingAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== Method Tracing Agent Started ===");
// Add class file transformer
inst.addTransformer(new MethodTracingTransformer(), true);
}
static class MethodTracingTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) throws IllegalClassFormatException {
// Skip system classes and our own agent classes
if (className == null || className.startsWith("java/") || 
className.startsWith("javax/") || className.startsWith("sun/") ||
className.startsWith("com/sun/") || className.startsWith("MethodTracingAgent")) {
return null; // Return null means no transformation
}
try {
return transformClass(className, classfileBuffer);
} catch (Exception e) {
System.err.println("Error transforming class: " + className);
e.printStackTrace();
return null;
}
}
private byte[] transformClass(String className, byte[] classfileBuffer) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
// Skip interfaces and abstract classes
if (ctClass.isInterface() || ctClass.isAnnotation() || ctClass.isArray()) {
return null;
}
boolean modified = false;
// Transform all methods
for (CtMethod method : ctClass.getDeclaredMethods()) {
// Skip constructors and static initializers
if (method.getName().equals("<init>") || method.getName().equals("<clinit>")) {
continue;
}
// Add tracing to the method
addTracingToMethod(method);
modified = true;
}
if (modified) {
System.out.println("Instrumented class: " + className);
return ctClass.toBytecode();
}
return null;
}
private void addTracingToMethod(CtMethod method) throws Exception {
// Create method signature for logging
String methodSignature = method.getDeclaringClass().getSimpleName() + "." + method.getName();
// Insert code at method start
method.insertBefore(
"System.out.println(\">>> Entering: " + methodSignature + "\");" +
"long startTime = System.nanoTime();"
);
// Insert code before return/throw
method.insertAfter(
"long duration = System.nanoTime() - startTime;" +
"System.out.println(\"<<< Exiting: " + methodSignature + " - took \" + (duration / 1000000.0) + \" ms\");",
false
);
// Also add to catch blocks for exceptions
method.addCatch(
"long duration = System.nanoTime() - startTime;" +
"System.out.println(\"!!! Exception in: " + methodSignature + " - took \" + (duration / 1000000.0) + \" ms\");" +
"throw $e;",
classPool.get("java.lang.Throwable")
);
}
}
}

4. Advanced Class Transformation

Performance Monitoring Agent

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Agent that monitors method performance and collects statistics
*/
public class PerformanceMonitorAgent {
private static final ConcurrentHashMap<String, MethodStats> methodStats = new ConcurrentHashMap<>();
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== Performance Monitor Agent Started ===");
// Add shutdown hook to print final statistics
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
printStatistics();
}));
// Add class transformer
inst.addTransformer(new PerformanceMonitoringTransformer(), true);
}
static class MethodStats {
AtomicLong callCount = new AtomicLong(0);
AtomicLong totalTime = new AtomicLong(0);
AtomicLong maxTime = new AtomicLong(0);
AtomicLong minTime = new AtomicLong(Long.MAX_VALUE);
void recordCall(long duration) {
callCount.incrementAndGet();
totalTime.addAndGet(duration);
maxTime.updateAndGet(current -> Math.max(current, duration));
minTime.updateAndGet(current -> Math.min(current, duration));
}
double getAverageTime() {
long count = callCount.get();
return count == 0 ? 0 : totalTime.get() / (double) count / 1_000_000.0;
}
}
static class PerformanceMonitoringTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
if (shouldSkipClass(className)) {
return null;
}
try {
return instrumentClass(className, classfileBuffer);
} catch (Exception e) {
System.err.println("Failed to instrument class: " + className);
return null;
}
}
private boolean shouldSkipClass(String className) {
return className == null || 
className.startsWith("java/") ||
className.startsWith("javax/") ||
className.startsWith("sun/") ||
className.startsWith("com/sun/") ||
className.contains("PerformanceMonitorAgent") ||
className.startsWith("javassist/");
}
private byte[] instrumentClass(String className, byte[] classfileBuffer) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
if (ctClass.isInterface() || ctClass.isAnnotation()) {
return null;
}
boolean modified = false;
for (CtMethod method : ctClass.getDeclaredMethods()) {
if (method.isEmpty() || method.getMethodInfo().isMethod()) {
continue;
}
if (instrumentMethod(method)) {
modified = true;
}
}
if (modified) {
System.out.println("Performance monitoring added to: " + className);
return ctClass.toBytecode();
}
return null;
}
private boolean instrumentMethod(CtMethod method) throws Exception {
String methodKey = method.getDeclaringClass().getName() + "#" + method.getName();
// Create unique field name for this method's stats
String statsFieldName = "methodStats_" + method.getName().hashCode();
// Ensure stats object exists
method.insertBefore(
"if (" + statsFieldName + " == null) {" +
"   " + statsFieldName + " = PerformanceMonitorAgent.getMethodStats(\"" + methodKey + "\");" +
"}"
);
// Insert timing code
method.insertBefore(
"long startTime = System.nanoTime();"
);
method.insertAfter(
"long duration = System.nanoTime() - startTime;" +
statsFieldName + ".recordCall(duration);",
false
);
// Add stats field to the class
CtField statsField = new CtField(
classPool.get("PerformanceMonitorAgent$MethodStats"), 
statsFieldName, 
method.getDeclaringClass()
);
statsField.setModifiers(Modifier.STATIC);
method.getDeclaringClass().addField(statsField);
return true;
}
}
public static MethodStats getMethodStats(String methodKey) {
return methodStats.computeIfAbsent(methodKey, k -> new MethodStats());
}
private static void printStatistics() {
System.out.println("\n=== Performance Statistics ===");
System.out.printf("%-60s %10s %12s %12s %12s%n", 
"Method", "Calls", "Avg(ms)", "Min(ms)", "Max(ms)");
System.out.println("=".repeat(120));
methodStats.entrySet().stream()
.sorted((e1, e2) -> Long.compare(e2.getValue().callCount.get(), e1.getValue().callCount.get()))
.limit(20) // Top 20 methods by call count
.forEach(entry -> {
MethodStats stats = entry.getValue();
if (stats.callCount.get() > 0) {
System.out.printf("%-60s %10d %12.3f %12.3f %12.3f%n",
entry.getKey(),
stats.callCount.get(),
stats.getAverageTime(),
stats.minTime.get() / 1_000_000.0,
stats.maxTime.get() / 1_000_000.0);
}
});
}
}

5. Dynamic Agent Attachment

Agent with Agentmain (Dynamic Attachment)

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
/**
* Agent that supports both premain and agentmain (dynamic attachment)
*/
public class DynamicAgent {
private static Instrumentation instrumentation;
/**
* Called when agent is started with JVM
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== Dynamic Agent Started via premain ===");
initializeAgent(agentArgs, inst);
}
/**
* Called when agent is attached to running JVM
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("=== Dynamic Agent Started via agentmain ===");
initializeAgent(agentArgs, inst);
}
private static void initializeAgent(String agentArgs, Instrumentation inst) {
instrumentation = inst;
System.out.println("Agent Arguments: " + agentArgs);
System.out.println("Instrumentation: " + inst);
// Parse agent arguments
String[] args = agentArgs != null ? agentArgs.split(",") : new String[0];
String mode = args.length > 0 ? args[0] : "monitor";
switch (mode) {
case "monitor":
startMonitoring();
break;
case "profile":
startProfiling();
break;
case "trace":
startTracing();
break;
default:
System.out.println("Unknown mode: " + mode);
}
}
private static void startMonitoring() {
System.out.println("Starting monitoring mode...");
instrumentation.addTransformer(new MonitoringTransformer(), true);
// Retransform loaded classes if possible
if (instrumentation.isRetransformClassesSupported()) {
try {
Class[] loadedClasses = instrumentation.getAllLoadedClasses();
for (Class clazz : loadedClasses) {
if (instrumentation.isModifiableClass(clazz) && 
!clazz.getName().startsWith("java.") &&
!clazz.getName().startsWith("sun.")) {
instrumentation.retransformClasses(clazz);
}
}
} catch (UnmodifiableClassException e) {
System.err.println("Failed to retransform classes: " + e.getMessage());
}
}
}
private static void startProfiling() {
System.out.println("Starting profiling mode...");
// Add profiling transformer
}
private static void startTracing() {
System.out.println("Starting tracing mode...");
// Add tracing transformer
}
// Getters for instrumented code
public static Instrumentation getInstrumentation() {
return instrumentation;
}
}

MANIFEST for Dynamic Agent

Manifest-Version: 1.0
Premain-Class: DynamicAgent
Agent-Class: DynamicAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Boot-Class-Path: javassist.jar
Build-Jdk: 11

6. Bytecode Manipulation Libraries

Using ASM for Bytecode Manipulation

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
/**
* Agent using ASM for bytecode manipulation
*/
public class ASMAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== ASM Agent Started ===");
inst.addTransformer(new ASMTransformer(), true);
}
static class ASMTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
if (shouldSkipClass(className)) {
return null;
}
try {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new MethodLoggingClassVisitor(writer, className);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
} catch (Exception e) {
System.err.println("ASM transformation failed for: " + className);
return null;
}
}
private boolean shouldSkipClass(String className) {
return className == null || 
className.startsWith("java/") ||
className.startsWith("javax/") ||
className.startsWith("sun/") ||
className.startsWith("org/objectweb/asm/");
}
}
static class MethodLoggingClassVisitor extends ClassVisitor {
private String className;
public MethodLoggingClassVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM9, cv);
this.className = className;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, 
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// Skip constructors and static initializers
if (name.equals("<init>") || name.equals("<clinit>")) {
return mv;
}
return new MethodLoggingMethodVisitor(mv, className, name, access, descriptor);
}
}
static class MethodLoggingMethodVisitor extends MethodVisitor {
private String className;
private String methodName;
public MethodLoggingMethodVisitor(MethodVisitor mv, String className, 
String methodName, int access, String descriptor) {
super(Opcodes.ASM9, mv);
this.className = className;
this.methodName = methodName;
}
@Override
public void visitCode() {
super.visitCode();
// Add logging at method entry
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("ENTER: " + className.replace('/', '.') + "." + methodName);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// Store start time
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 1); // Store in local variable 1
}
@Override
public void visitInsn(int opcode) {
// Add logging before return instructions
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
addExitLogging();
}
super.visitInsn(opcode);
}
private void addExitLogging() {
// Calculate duration
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(Opcodes.LLOAD, 1); // Load start time
mv.visitInsn(Opcodes.LSUB); // Subtract start from current
mv.visitVarInsn(Opcodes.LSTORE, 3); // Store duration
// Print exit message with duration
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("EXIT: " + className.replace('/', '.') + "." + methodName + " - ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(Opcodes.LLOAD, 3); // Load duration
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" ns");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}

7. Real-World Use Cases

Security Agent - Method Access Control

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
import java.util.*;
/**
* Security agent that enforces method access rules
*/
public class SecurityAgent {
private static final Set<String> RESTRICTED_METHODS = new HashSet<>();
private static final Set<String> ALLOWED_PACKAGES = new HashSet<>();
static {
// Configure restricted methods
RESTRICTED_METHODS.add("java.lang.Runtime.exec");
RESTRICTED_METHODS.add("java.lang.System.exit");
RESTRICTED_METHODS.add("java.io.File.delete");
// Configure allowed packages
ALLOWED_PACKAGES.add("com.yourapp.");
ALLOWED_PACKAGES.add("org.springframework.");
}
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== Security Agent Started ===");
// Parse configuration from agent args
if (agentArgs != null) {
parseConfiguration(agentArgs);
}
inst.addTransformer(new SecurityTransformer(), true);
}
private static void parseConfiguration(String agentArgs) {
String[] configs = agentArgs.split(";");
for (String config : configs) {
if (config.startsWith("restrict:")) {
String methods = config.substring("restrict:".length());
Collections.addAll(RESTRICTED_METHODS, methods.split(","));
} else if (config.startsWith("allow:")) {
String packages = config.substring("allow:".length());
Collections.addAll(ALLOWED_PACKAGES, packages.split(","));
}
}
}
static class SecurityTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
if (className == null || isAllowedClass(className)) {
return null;
}
try {
return addSecurityChecks(className, classfileBuffer);
} catch (Exception e) {
System.err.println("Security transformation failed: " + e.getMessage());
return null;
}
}
private boolean isAllowedClass(String className) {
String dotClassName = className.replace('/', '.');
return ALLOWED_PACKAGES.stream().anyMatch(dotClassName::startsWith);
}
private byte[] addSecurityChecks(String className, byte[] classfileBuffer) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
if (ctClass.isInterface()) {
return null;
}
boolean modified = false;
for (CtMethod method : ctClass.getDeclaredMethods()) {
if (needsSecurityCheck(method)) {
addSecurityCheck(method);
modified = true;
}
}
if (modified) {
System.out.println("Security checks added to: " + className);
return ctClass.toBytecode();
}
return null;
}
private boolean needsSecurityCheck(CtMethod method) {
String methodSignature = method.getDeclaringClass().getName() + "." + method.getName();
return RESTRICTED_METHODS.contains(methodSignature);
}
private void addSecurityCheck(CtMethod method) throws Exception {
method.insertBefore(
"throw new SecurityException(\"Access denied to restricted method: " + 
method.getDeclaringClass().getName() + "." + method.getName() + "\");"
);
}
}
}

Dependency Injection Agent

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import javassist.*;
import java.lang.reflect.Field;
import java.util.*;
/**
* Agent that provides dependency injection capabilities
*/
public class DIAgent {
private static final Map<String, Object> beanCache = new HashMap<>();
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=== DI Agent Started ===");
inst.addTransformer(new DITransformer(), true);
}
static class DITransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
if (className == null || className.startsWith("java/") || className.contains("DIAgent")) {
return null;
}
try {
return injectDependencies(className, classfileBuffer);
} catch (Exception e) {
System.err.println("DI transformation failed: " + e.getMessage());
return null;
}
}
private byte[] injectDependencies(String className, byte[] classfileBuffer) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
// Check if class has @Component annotation
if (!hasComponentAnnotation(ctClass)) {
return null;
}
System.out.println("Processing component: " + className);
boolean modified = false;
// Process fields with @Autowired
for (CtField field : ctClass.getDeclaredFields()) {
if (hasAutowiredAnnotation(field)) {
injectField(field, ctClass);
modified = true;
}
}
if (modified) {
return ctClass.toBytecode();
}
return null;
}
private boolean hasComponentAnnotation(CtClass ctClass) throws Exception {
try {
Object[] annotations = ctClass.getAnnotations();
for (Object ann : annotations) {
if (ann.toString().contains("@Component")) {
return true;
}
}
} catch (Exception e) {
// Ignore annotation parsing errors
}
return false;
}
private boolean hasAutowiredAnnotation(CtField field) throws Exception {
try {
Object[] annotations = field.getAnnotations();
for (Object ann : annotations) {
if (ann.toString().contains("@Autowired")) {
return true;
}
}
} catch (Exception e) {
// Ignore annotation parsing errors
}
return false;
}
private void injectField(CtField field, CtClass declaringClass) throws Exception {
String fieldType = field.getType().getName();
String fieldName = field.getName();
// Create static initializer if it doesn't exist
CtConstructor classInitializer = null;
try {
classInitializer = declaringClass.getClassInitializer();
} catch (NotFoundException e) {
classInitializer = CtNewConstructor.make("static {}", declaringClass);
declaringClass.addConstructor(classInitializer);
}
// Add injection code to static initializer
String injectionCode = 
"try {" +
"   java.lang.reflect.Field f = " + declaringClass.getName() + ".class.getDeclaredField(\"" + fieldName + "\");" +
"   f.setAccessible(true);" +
"   Object bean = DIAgent.getBean(\"" + fieldType + "\");" +
"   if (bean != null) {" +
"       f.set(null, bean);" + // For static fields
"   }" +
"} catch (Exception e) {" +
"   System.err.println(\"Failed to inject field " + fieldName + ": \" + e.getMessage());" +
"}";
classInitializer.insertAfter(injectionCode);
System.out.println("Added DI for field: " + fieldName + " of type: " + fieldType);
}
}
// Bean management methods
public static void registerBean(String name, Object bean) {
beanCache.put(name, bean);
}
public static Object getBean(String className) {
return beanCache.get(className);
}
public static <T> T getBean(Class<T> clazz) {
return clazz.cast(beanCache.get(clazz.getName()));
}
}

8. Testing and Debugging Agents

Test Application

// Simple test application to demonstrate agent functionality
public class TestApplication {
public static void main(String[] args) throws InterruptedException {
System.out.println("Test Application Started");
TestService service = new TestService();
for (int i = 0; i < 5; i++) {
service.processRequest("Request-" + i);
Thread.sleep(1000);
}
System.out.println("Test Application Completed");
}
static class TestService {
public void processRequest(String request) {
System.out.println("Processing: " + request);
try {
Thread.sleep(500); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public String getData() {
return "Sample Data";
}
}
}

Agent Testing Framework

import java.lang.instrument.Instrumentation;
import java.util.*;
/**
* Framework for testing Java agents
*/
public class AgentTester {
public static void testAgent(String agentJar, String testClass, String[] args) throws Exception {
// This would typically use the Attach API to load the agent
System.out.println("Testing agent: " + agentJar);
System.out.println("Test class: " + testClass);
// For demonstration, we'll simulate agent testing
simulateAgentTesting(agentJar, testClass, args);
}
private static void simulateAgentTesting(String agentJar, String testClass, String[] args) {
System.out.println("=== Agent Test Results ===");
System.out.println("Agent JAR: " + agentJar);
System.out.println("Test Class: " + testClass);
System.out.println("Arguments: " + Arrays.toString(args));
// Simulate various test scenarios
testClassLoading();
testMethodInstrumentation();
testPerformanceImpact();
testMemoryUsage();
}
private static void testClassLoading() {
System.out.println("\n1. Class Loading Test:");
System.out.println("   ✓ Classes load successfully with agent");
System.out.println("   ✓ No ClassFormatError exceptions");
System.out.println("   ✓ No VerifyError exceptions");
}
private static void testMethodInstrumentation() {
System.out.println("\n2. Method Instrumentation Test:");
System.out.println("   ✓ Methods are properly instrumented");
System.out.println("   ✓ Instrumentation doesn't break method logic");
System.out.println("   ✓ Exception handling works correctly");
}
private static void testPerformanceImpact() {
System.out.println("\n3. Performance Impact Test:");
System.out.println("   ✓ Acceptable performance overhead (< 10%)");
System.out.println("   ✓ No significant memory leak detected");
System.out.println("   ✓ GC behavior remains stable");
}
private static void testMemoryUsage() {
System.out.println("\n4. Memory Usage Test:");
System.out.println("   ✓ Agent memory footprint is reasonable");
System.out.println("   ✓ No class metadata leaks");
System.out.println("   ✓ Transformer doesn't retain excessive memory");
}
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("Usage: AgentTester <agent-jar> <test-class> [agent-args]");
return;
}
String agentJar = args[0];
String testClass = args[1];
String[] agentArgs = args.length > 2 ? Arrays.copyOfRange(args, 2, args.length) : new String[0];
testAgent(agentJar, testClass, agentArgs);
}
}

9. Best Practices

1. Error Handling

public class RobustAgent {
public static void premain(String agentArgs, Instrumentation inst) {
try {
// Initialize agent
initializeAgent(agentArgs, inst);
} catch (Throwable t) {
// Don't let agent crash the JVM
System.err.println("Agent initialization failed: " + t.getMessage());
t.printStackTrace();
}
}
private static void initializeAgent(String agentArgs, Instrumentation inst) {
// Agent initialization code
inst.addTransformer(new SafeTransformer(), true);
}
static class SafeTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
try {
return doTransform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
} catch (Throwable t) {
// Log error but don't break class loading
System.err.println("Transformation failed for " + className + ": " + t.getMessage());
return null; // Return null to use original class bytes
}
}
private byte[] doTransform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
// Actual transformation logic
return null;
}
}
}

2. Performance Considerations

public class EfficientAgent {
private static final Set<String> processedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>());
static class EfficientTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, 
Class<?> classBeingRedefined, ProtectionDomain protectionDomain, 
byte[] classfileBuffer) {
// Skip if already processed
if (!processedClasses.add(className)) {
return null;
}
// Fast class filtering
if (className == null || !shouldInstrument(className)) {
return null;
}
// Use efficient bytecode manipulation
return transformEfficiently(className, classfileBuffer);
}
private boolean shouldInstrument(String className) {
// Fast path checks first
if (className.startsWith("java/") || className.startsWith("sun/")) {
return false;
}
// More expensive checks only if necessary
return className.contains("/service/") || className.contains("/controller/");
}
}
}

10. Deployment and Usage

Command Line Usage

# Basic agent usage
java -javaagent:myagent.jar -jar myapp.jar
# With agent arguments
java -javaagent:myagent.jar=mode=profile,threshold=100ms -jar myapp.jar
# Multiple agents
java -javaagent:agent1.jar -javaagent:agent2.jar=debug -jar myapp.jar
# Attach to running JVM (requires tools.jar)
jcmd <pid> VM.load_agent myagent.jar=args

Build Script (Maven)

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.example.MyAgent</Premain-Class>
<Agent-Class>com.example.MyAgent</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>
<Boot-Class-Path>javassist.jar</Boot-Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>

Summary

Java Agents provide powerful capabilities for:

  • Runtime class transformation - Modify classes as they load
  • Performance monitoring - Track method execution times
  • Security enforcement - Add access controls
  • Dependency injection - Automatically wire components
  • Debugging and profiling - Add logging and metrics

Key considerations:

  • Always handle errors gracefully
  • Minimize performance impact
  • Use efficient bytecode manipulation
  • Test thoroughly with different JVMs
  • Consider security implications

Java Agents are used by many popular tools like JRebel, YourKit, and application performance monitoring (APM) tools.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper