Home > Articles > Programming > Java

Java Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

Java Agents

Last updated Mar 14, 2003.

In the early days of bytecode instrumentation, most implementations were built using native code (most of it was written in C or C++), but the Java Virtual Machine provides support for Java-based agents through the -javaagent:path-to-jar-file command line option. It is arguable as to whether or not using a native agent is any better than using a Java agent because the instrumentation process is only performed during class loading – it is the instrumented class code that has a more measurable impact on performance. In order to create a Java agent, you need to perform the following steps:

  1. Create an agent class, which is a class that defines a premain() method
  2. Define a class file transformer class, which is a class that implements ClassFileTransformer and implements its transform() method
  3. The premain() method passes you an Instrumentation class, you need to add your class file transformer to its list of transformers
  4. Define your own MANIFEST.MF file for your JAR file that defines a Premain-Class attribute that points to your agent and configure specific permissions that it will need to be able to transform classes
  5. Build a JAR file that contains your agent and class file transformer and your custom MANIFEST.MF file
  6. Execute your target application with the -javaagent:path-to-your-jar-file command line option

Listing 2 shows the source code for the Java Agent.

Listing 2. Agent.java

package com.geekcap.openapm.jvm.agent;

import java.lang.instrument.Instrumentation;
import org.apache.log4j.Logger;

/**
 * Tracing Agent
 * 
 * @author shaines
 */
public class Agent
{
    private static final Logger logger = Logger.getLogger( Agent.class );

    /**
     * A reference to the Java Instrumentation object
     */
    private Instrumentation instrumentation;

    /**
     * Our bytecode transformer
     */
    private JavaAssistTransformer transformer;

    /**
     * Creates a new Agent
     *
     * @param instrumentation
     */
    public Agent( Instrumentation instrumentation )
    {
        // Save a reference to the Instrumentation class
        this.instrumentation = instrumentation;

        // Create our Transformer
        transformer = new JavaAssistTransformer( instrumentation );
    }

    public static void premain( String agentArgs, Instrumentation inst )
    {
        if( logger.isInfoEnabled() )
        {
            logger.info( "In premain, creating the OpenAPM Agent" );
        }
        Agent agent = new Agent( inst );
    }
}

The Agent class defines a premain() method that accepts agent arguments as a String and an Instrumentation instance. The premain() method creates an instance of the Agent class and in its constructor it creates an instance of the JavaAssistTransformer class, passing it a reference to the Instrumentation instance.

Listing 3 shows the skeleton code for the JavaAssistTransformer class, which we'll be exploring in more detail in the next section.

Listing 3. JavaAssistTransformer.java

package com.geekcap.openapm.jvm.agent;

import com.geekcap.openapm.trace.TraceManager;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import org.apache.log4j.Logger;

/**
 * Class file transformer that uses the Javassist bytecode instrumentation library
 * to inject calls to the OpenAPM Tracer to trace the paths of requests through a
 * Java application
 *
 * @author shaines
 */
public class JavaAssistTransformer implements ClassFileTransformer
{
    private static final Logger logger = Logger.getLogger( JavaAssistTransformer.class );
    
    /**
     * The underlying Java Instrumentation object
     */
    protected Instrumentation instrumentation = null;

    /**
     * Creates a new JavaAssistTransformer and sets up the Javassist environment
     *
     * @param instrumentation           The Java Instrumentation object to which we
     *                                  add ourselves as a transformer
     */
    public JavaAssistTransformer( Instrumentation instrumentation )
    {
        // Save our instrumentation object
        this.instrumentation = instrumentation;

        // Add our transformer to the list of transformers
        this.instrumentation.addTransformer( this );

        // Start the TraceManager
        TraceManager.getInstance().startTraceManager();
    }

    /**
     * This method is invoked by the JVM for on all transformers to provide a mechanism for
     * the transformer to modify the bytecode of classes as they are being loaded by the
     * JVM.
     *
     * @param loader                        The ClassLoader that is loading this class
     * @param className                     The name of the class being loaded
     * @param classBeingRedefined           If this is triggered by a redefine or retransform,
     *                                      the class being redefined or retransformed; if this is a class
                                            load, null
     * @param protectionDomain              The protection domain of the class being defined or redefined
     * @param classfileBuffer               The input byte buffer in class file format - must not be modified
     *
     * @return                              A well-formed class file buffer (the result of the transform),
                                            or null if no transform is performed.
     *
     * @throws IllegalClassFormatException  If the input does not represent a well-formed class file
     */
    @Override
    public byte[] transform( ClassLoader loader, 
                             String className,
                             Class classBeingRedefined,
                             ProtectionDomain protectionDomain,
                             byte[] classfileBuffer ) throws IllegalClassFormatException
    {
        // Returning null means that we're going to use the uninstrumented bytecode
        return null;
    }
}

The JavaAssistTransformer class extends java.lang.instrument.ClassFileTransformer and provides an empty implementation of the transform() method. The transform() method receives a byte array (classfileBuffer) and, if it wants to transform the class (which is typically determined by the class name), then it returns the modified byte code array, otherwise it returns null to tell the class loader that it is not modifying the byte code.

The other worthy point of mention in the JavaAssistTransformer is that in the constructor it adds itself to the list of class file transformers by invoking the Instrumentation class's addTransformer() method. Without this method call, the transform() method would never be invoked.

Listing 4 shows the contents of the MANIFEST.MF file, which will need to be placed in the META-INF folder inside your JAR file.

Listing 4. MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: com.geekcap.openapm.jvm.agent.Agent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true

The MANIFEST.MF file defines the name of the class that contains the premain() method, which is our Agent class, and defines three required permissions:

  • Can-Redefine-Classes
  • Can-Retransform-Classes
  • Can-Set-Native-Method-Prefix

With these permissions set, you will be able to transform classes in your class file transformer. Again, the MANIFEST.MF file must be located in your JAR file's META-INF directory.

When you launch your application with the -javaagent:<path-to-your-JAR-file> command line argument, the JVM will open this JAR file, look for the MANIFEST.MF file, and then invoke the Premain-Class's premain() method.