ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Achieving Inversion of Control with Eclipse RCP
Pages: 1, 2, 3, 4

ASM and java.lang.instrument Agents

The various injection strategies described in the previous section usually depend on the presence of a container to provide an entry point, which the application uses to request properly configured objects. However, we want to maintain a transparent approach while developing our IoC plugin for two reasons:



  • RCP adopts complex classloader and instantiation strategies (think of createExecutableExtension()) to maintain plugin isolation and enforce visibility constraints. We do not want to modify or substitute such strategies to introduce our container-based initialization rules.
  • An explicit reference to such an entry point (in our case, the service() method defined in the Service Locator plugin) will force the application developer to adopt an explicit pattern and logic to retrieve initialized components. This represents a sort of library lock-in on the application code. We want to define a collaborating plugin that does not require an explicit reference to its codebase.

For these reasons, we'll introduce java transforming agents as defined in the java.lang.instrument package, available from J2SE 5.0 and higher. A transforming agent is an object that implements the java.lang.instrument.ClassFileTransformer interface that defines a single transform() method. When a transformer instance is registered to the JVM, that transformer instance will be called for each class being created in the JVM. The transformer gets access to the class bytecode and can modify the class representation before it is loaded by the JVM.

Transforming agents can be registered to the JVM using command line parameters of the form -javaagent:jarpath[=options], where jarpath is the path to the JAR file containing the agent class, and options is a parameter string for the agent. The agent JAR file uses a special manifest attribute to specify the actual agent class, which must define a method public static void premain(String options, Instrumentation inst). This agent premain() method will be called before the application's main() method and is able to register an actual transformer with the passed-in java.lang.instrument.Instrumentation class instance.

In our sample, we define an agent that performs bytecode manipulation to transparently add runtime invocations to our IoC container (the Service Locator plugin). The agent will identify serviceable objects by verifying the presence of the Serviceable annotation on the class. It will then modify all of the available constructors adding callbacks to the IoC container so that it can configure and initialize the object at instantiation time.

Let's suppose we have this object that depends on external services (remember the Injected annotation):

@Serviceable
public class ServiceableObject {
  
  public ServiceableObject() {

    System.out.println("Initializing...");
  }

  @Injected public void aServicingMethod(
    Service s1, 
    AnotherService s2) {

    // ... omissis ...
  }

}

After the modification is performed by the agent, its bytecode will be the same as that obtained by normal compilation of this class:

@Serviceable
public class ServiceableObject {
  
  public ServiceableObject() {
    ServiceLocator.service(this);
    System.out.println("Initializing...");
  }

  @Injected public void aServicingMethod(
    Service s1, 
    AnotherService s2) {

    // ... omissis ...
  }

}

With this solution, we are now able to properly configure serviceable objects and make them available to the application without needing the developer to hardwire the dependency on the container. The developer will only have to mark serviceable objects with the Serviceable annotation. The agent code follows:

public class IOCTransformer 
  implements ClassFileTransformer {

    public byte[] transform(
      ClassLoader loader, 
      String className,
            Class<?> classBeingRedefined, 
      ProtectionDomain protectionDomain,
            byte[] classfileBuffer) 
      throws IllegalClassFormatException {
        
        System.out.println("Loading " + className);        
        ClassReader creader = 
      new ClassReader(classfileBuffer);

        // Parse the class file
        ConstructorVisitor cv = 
      new ConstructorVisitor();
        ClassAnnotationVisitor cav =
      new ClassAnnotationVisitor(cv);        

        creader.accept(cav, true);

        if (cv.getConstructors().size() > 0) {
            System.out.println("Enhancing "+className);
            // Generate the enhanced-constructor class
            ClassWriter cw = new ClassWriter(false);
            ClassConstructorWriter writer = 
        new ClassConstructorWriter(
          cv.getConstructors(),
          cw);        
            creader.accept(writer, false);
            
      return cw.toByteArray();

        }
        else
            return null;
    }
    
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new IOCTransformer());
    }

}

The classes ConstructorVisitor, ClassAnnotationVisitor, ClassWriter, and ClassConstructorWriter perform the bytecode manipulation using the ObjectWeb ASM library.

ASM uses the visitor pattern to process class data (including instruction sequences) as streams of events. When decoding an existing class, ASM generates the stream of events for us, calling our methods for processing the events. When generating a new class, the opposite happens: we generate a stream of events, which the ASM library converts into a generated class. Notice that the described approach does not depend on the specific bytecode library in use (ASM in our case); other available solutions such as BCEL or Javassist may do the work as well.

We will not dig into the internals of ASM. For the purpose of this article, it is sufficient to know that ConstructorVisitor and ClassAnnotationVisitor objects are used to identify classes tagged with the Serviceable annotation and collect their constructors. Their source code follows:

        
public class ClassAnnotationVisitor 
  extends ClassAdapter {
    
    private boolean matches = false;

    public ClassAnnotationVisitor(ClassVisitor cv) {
        super(cv);
    }
    
    @Override
    public AnnotationVisitor visitAnnotation(
    String desc, 
    boolean visible) {
        
        if (visible &&
      desc.equals("Lcom/onjava/servicelocator/annot/Serviceable;")) {
            matches = true;        
        }
        
        return super.visitAnnotation(desc, visible);
    }
    
    @Override
    public MethodVisitor visitMethod(
    int access, 
    String name, 
    String desc, 
    String signature, 
    String[] exceptions) {

    if (matches)
            return super.visitMethod(
        access,name,desc,signature,exceptions);
        else {
            return null;
        }
    }
    
}

public class ConstructorVisitor 

  extends EmptyVisitor {
    
    private Set<Method> constructors;

    public ConstructorVisitor() {
        constructors = new HashSet<Method>();
    }
    
    public Set<Method> getConstructors() {
        return constructors;
    }
        
    @Override
    public MethodVisitor visitMethod(
    int access, 
    String name, 
    String desc, 
    String signature, 
    String[] exceptions) {
        
        Type t = Type.getReturnType(desc);
        
        if (name.indexOf("<init>") != -1 &&
        t.equals(Type.VOID_TYPE)) {

            constructors.add(new Method(name,desc));            
        }
        
        return super.visitMethod(
      access,name,desc,signature,exceptions);
    }
}

For every constructor collected by the previous classes, an instance of ClassConstructorWriter modifies it by injecting the following call to the Service Locator plugin:

com.onjava.servicelocator.ServiceLocator.service(this);

The ASM way to do the work requires these instructions:

// mv is an ASM method visitor,
// a class which allows method manipulation
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(
    INVOKESTATIC, 
    "com/onjava/servicelocator/ServiceLocator", 
    "service", 
    "(Ljava/lang/Object;)V");

The first instruction loads the this object reference onto the stack that will be used in the second instruction, which is the invocation of a static method of the ServiceLocator class.

Pages: 1, 2, 3, 4

Next Pagearrow