Back.gif

Background

The only requirement for a class used to register a Java Agent is a premain method:
package schuchert.agent;
 
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
 
public class ConfigurableClassFileTransformerRedirector {
    public static void premain(String agentArguments, Instrumentation instrumentation) {
    }
}

This is a complete, minimal example. If this class is packaged in a jar file with a correct manifest, when the VM starts up, the premain method gets called and has a chance to register an instance of a ClassFileTransformer with the instrumentation instance.

registrar

Here's a simple class that will report the unqualified class name and its package:
package schuchert.agent;
 
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
 
public class ClassAndPackageNamePrintingClassFileTransformer implements ClassFileTransformer {
 
    public byte[] transform(ClassLoader loader, String fullyQualifiedClassName, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classofileBuffer) throws IllegalClassFormatException {
        String className = fullyQualifiedClassName.replaceAll(".*/", "");
        String pacakge = fullyQualifiedClassName.replaceAll("/[a-zA-Z$0-9_]*$", "");
        System.out.printf("Class: %s in: %s\n", className, pacakge);
        return null;
    }
 
}

To get this ClassFileTransformer registered, we'd change pre-main to the following:
public static void premain(String agentArguments, Instrumentation instrumentation) {
    instrumentation.addTransformer(new ClassAndPackageNamePrintingClassFileTransformer());
}

ConfigurableClassFileTransformerRegistrar

Below is the source for the registrar. Here are a couple of notes:
  • Many of the output strings are exposed as package-level constants to make testing easier
  • There's a static filed, ERROR_OUT, exposed for testability as well
  • This class records the each class it instantiates in registeredTransformers, again for testability
  • This class allows a : separated list of classes. E.g. rather than using multiple -javaagent: lines or passing parameters into the premain(...), I've choses to support a : separated list.
  • If you want to see the tests that verify this class' functionality, look further below. Note that the test class uses JUnit 4.4 and JMock 2.4.


package schuchert.agent;
 
import java.io.PrintStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
 
public class ConfigurableClassFileTransformerRegistrar {
    private static List<ClassFileTransformer> registeredTransformers = new LinkedList<ClassFileTransformer>();
 
    public static final String DEST_CLASS_PROPERTY_NAME = "schuchert.ClassFileTransformer";
 
    static final String INSTANTIATION_ERROR = "Unable to instantiate: %s\n";
    static final String CHECK_SYSTEM_PROPERTY_MESSAGE = "Please check setting for system property: %s\n";
    static final String EXAMPLE_MESSAGE = "e.g. -D%s=%s\n";
 
    static PrintStream ERROR_OUT = System.err;
 
    public static void premain(String agentArguments, Instrumentation instrumentation) {
        for (String className : getClassNames()) {
            addOneClassFileTransformer(instrumentation, className);
        }
    }
 
    public static Iterator<ClassFileTransformer> iterator() {
        return registeredTransformers.iterator();
    }
 
    private static void addOneClassFileTransformer(Instrumentation instrumentation, String className) {
        ClassFileTransformer classFileTransformer = createTargetTransformer(className);
        if (classFileTransformer != null) {
            instrumentation.addTransformer(classFileTransformer);
            registeredTransformers.add(classFileTransformer);
        }
    }
 
    private static ClassFileTransformer createTargetTransformer(String className) {
        ClassFileTransformer targetTransformer = null;
 
        if (className != null) {
            try {
                Class<?> targetTransformerClass = Class.forName(className);
                targetTransformer = (ClassFileTransformer) targetTransformerClass.newInstance();
            } catch (Exception e) {
                reportException(e, className);
            }
        }
 
        return targetTransformer;
 
    }
 
    private static String[] getClassNames() {
        String classNameList = System.getProperty(DEST_CLASS_PROPERTY_NAME);
 
        if (classNameListMissingOrEmpty(classNameList)) {
            reportUsage();
            return new String[] {};
        }
 
        String[] classNames = classNameList.split(":");
        for (int i = 0; i < classNames.length; ++i) {
            classNames[i] = classNames[i].trim();
        }
        return classNames;
    }
 
    private static boolean classNameListMissingOrEmpty(String classNameList) {
        return classNameList == null || classNameList.trim().length() == 0;
    }
 
    private static void reportException(Exception e, String className) {
        e.printStackTrace(ERROR_OUT);
        ERROR_OUT.printf(INSTANTIATION_ERROR, className);
        reportUsage();
    }
 
    private static void reportUsage() {
        ERROR_OUT.printf(CHECK_SYSTEM_PROPERTY_MESSAGE, DEST_CLASS_PROPERTY_NAME);
        ERROR_OUT.printf(EXAMPLE_MESSAGE, DEST_CLASS_PROPERTY_NAME, NullClassFileTransformer.class.getName());
    }
}

ConfigurableClassFileTransformerRegistrarTest

package schuchert.agent;
 
import java.io.PrintStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.util.Properties;
 
import junit.framework.Assert;
 
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
 
import util.MockeryUtil;
 
@RunWith(JMock.class)
public class ConfigurableClassFileTransformerRegistrarTest {
    Mockery context = MockeryUtil.createMockeryForConcreteClasses();
    Instrumentation insrumentationSpy;
    PrintStream errorOutSpy;
 
    @Before
    public void createInsrumentationSpy() {
        insrumentationSpy = context.mock(Instrumentation.class);
    }
 
    @Before
    public void createAndRegisterErrorOutSpy() {
        errorOutSpy = context.mock(PrintStream.class);
        ConfigurableClassFileTransformerRegistrar.ERROR_OUT = errorOutSpy;
    }
 
    @Before
    public void clearSystemProperty() {
        Properties systemProperties = System.getProperties();
        systemProperties.remove(ConfigurableClassFileTransformerRegistrar.DEST_CLASS_PROPERTY_NAME);
        System.setProperties(systemProperties);
        Assert.assertNull(System.getProperty(ConfigurableClassFileTransformerRegistrar.DEST_CLASS_PROPERTY_NAME));
    }
 
    @Test
    public void errorReportedAndNoClassRegisteredWhenNoSystemPropertyDefined() {
        expectMissingSystemPropertyMessage();
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void systemPropertyDefinedButNullAlwaysPassesBecuaseNotPossibleToCreaetNullSystemProperty() {
    }
 
    @Test
    public void missingSystemPropertyReportedAndNoClassRegisteredWhenSystemPropertyEmptyString() {
        expectMissingSystemPropertyMessage();
 
        setSystemProperty("");
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void missingSystemPropertyReportedAndNoClassRegisteredWhenSystemPropertyContainsNothingButSpaces() {
        expectMissingSystemPropertyMessage();
 
        setSystemProperty("   ");
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void noErrorsWhenSystemPropertySetToSingleClassThatExistsInClassPath() {
        expectRegistryOfNullClassFileTransformer();
 
        setSystemProperty(NullClassFileTransformer.class.getName());
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void noErrorsWhenSystemPropertySetToSingleClassWithSpacesOnEndsThatExistsInClassPath() {
        expectRegistryOfNullClassFileTransformer();
 
        setSystemProperty("   " + NullClassFileTransformer.class.getName() + "   ");
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void noErrorsWhenSystemPropertySetToMultipleClassesAllOfWhichAreInClassPath() {
        expectRegistryOfNullClassFileTransformer();
        expectRegistryOf(ClassAndPackageNamePrintingClassFileTransformer.class);
 
        setSystemProperty(NullClassFileTransformer.class.getName() + ":"
                + ClassAndPackageNamePrintingClassFileTransformer.class.getName());
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void noErrorsWhenSystemPropertySetToMultipleClassesWithSpacesAtEndsOfNames() {
        expectRegistryOfNullClassFileTransformer();
        expectRegistryOf(ClassAndPackageNamePrintingClassFileTransformer.class);
 
        setSystemProperty(" " + NullClassFileTransformer.class.getName() + " : "
                + ClassAndPackageNamePrintingClassFileTransformer.class.getName() + " ");
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void noClassRegisteredWhenSingleMissingClassInSystemProperty() {
        expectReportOfClassNotFound();
 
        setSystemProperty("MissingClassThatShouldNotExistInClassPathEver");
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void nullTransformerRegisteredWhenSingleBadClassNameInSystemProperty() {
        expectReportOfClassNotFound();
 
        setSystemProperty("InvalidClassName^^^^");
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void nullTransformerRegisteredForEachBadClassWhenBothGoodAndBadClassNamesInSystemProperty() {
        expectRegistryOfNullClassFileTransformer();
        expectReportOfClassNotFound();
        expectRegistryOf(ClassAndPackageNamePrintingClassFileTransformer.class);
 
        setSystemProperty(NullClassFileTransformer.class.getName() + ":InvalidClassname^^^:"
                + ClassAndPackageNamePrintingClassFileTransformer.class.getName());
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    @Test
    public void classNotImplementingClassFileFormaterErrorReported() {
        expectClassCastException();
        expectReportOfMissingClass();
        expectMissingSystemPropertyMessage();
 
        setSystemProperty(getClass().getName());
 
        ConfigurableClassFileTransformerRegistrar.premain("", insrumentationSpy);
    }
 
    private void setSystemProperty(String value) {
        System.setProperty(ConfigurableClassFileTransformerRegistrar.DEST_CLASS_PROPERTY_NAME, value);
    }
 
    private void expectMissingSystemPropertyMessage() {
        context.checking(new Expectations() {
            {
                one(errorOutSpy).printf(with(equal(ConfigurableClassFileTransformerRegistrar.CHECK_SYSTEM_PROPERTY_MESSAGE)),
                        with(any(String.class)));
                one(errorOutSpy).printf(with(equal(ConfigurableClassFileTransformerRegistrar.EXAMPLE_MESSAGE)),
                        with(any(Object.class)));
            }
        });
    }
 
    private void expectRegistryOfNullClassFileTransformer() {
        expectRegistryOf(NullClassFileTransformer.class);
    }
 
    private void expectRegistryOf(final Class<?> clazz) {
        context.checking(new Expectations() {
            {
                one(insrumentationSpy).addTransformer((ClassFileTransformer) with(any(clazz)));
            }
        });
    }
 
    private void expectReportOfMissingClass() {
        context.checking(new Expectations() {
            {
                one(errorOutSpy).printf(with(equal(ConfigurableClassFileTransformerRegistrar.INSTANTIATION_ERROR)),
                        with(any(String.class)));
            }
        });
 
    }
 
    private void expectClassNotFoundExceptionReported() {
        context.checking(new Expectations() {
            {
                one(errorOutSpy).println(with(any(ClassNotFoundException.class)));
                ignoring(errorOutSpy).println(with(any(String.class)));
            }
        });
    }
 
    private void expectReportOfClassNotFound() {
        expectClassNotFoundExceptionReported();
        expectReportOfMissingClass();
        expectMissingSystemPropertyMessage();
    }
 
    private void expectClassCastException() {
        context.checking(new Expectations() {
            {
                one(errorOutSpy).println(with(any(ClassCastException.class)));
                ignoring(errorOutSpy).println(with(any(String.class)));
            }
        });
 
    }
 
}