ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Achieving Inversion of Control with Eclipse RCP

by Riccardo Govoni
06/07/2006

Eclipse Rich Client Platform (RCP) is a powerful software foundation, based on interconnected and collaborating plugins, that allows the developer to build generic applications. RCP leaves the developer free to concentrate on the code that performs the application business, instead of spending time reinventing the wheel to write the application managing logic.

Inversion of Control (IoC) and Dependency Injection (DI) are programming patterns that can be used to reduce coupling in computer programs. They follow a simple principle: you do not create your objects; you describe how they should be created. You do not instantiate or directly locate the services needed by your components; instead you describe which services are needed by which components, and someone else (generally a container) is responsible for hooking it all together. This is also known as the Hollywood principle: don't call us--we'll call you.

This article will describe a simple way to introduce support for Dependency Injection into an Eclipse RCP application. To avoid polluting the Eclipse platform infrastructure and to transparently add the IoC framework on top of the RCP, we will use a combination of runtime bytecode manipulation techniques (using the ObjectWeb ASM library), Java classloading agents (using the java.lang.instrument package), and Java annotations.

What is Eclipse Rich Client Platform?

In a single sentence, Eclipse Rich Client Platform is a set of libraries, software frameworks, and a runtime environment for building applications that are both stand-alone and strongly interconnected with the network.

Although Eclipse was conceived as a framework to build Integrated Development Environments (IDEs), the whole product has been refactored (starting from the 3.0 release) into various separated components, so that a minimum subset of these components can be used to build arbitrary applications. Such a subset makes up the Rich Client Platform and comprises different elements: the basic runtime, the user interface components (SWT and JFace), the plugin, and OSGi layer. Figure 1 shows the main components of the Eclipse platform.

The main Eclipse platform components
Figure 1. The Eclipse platform's main components

The entire Eclipse platform is based on the key concepts of plugins and extensions points. A plugin is the smallest unit of function that can be developed and delivered separately. It is typically packaged as a jar file and extends the platform by adding a functionality (e.g., an editor, a toolbar button, or a compiler). The whole platform is simply a set of interconnected and communicating plugins. An extension point is an available interconnection endpoint that other plugins may use to provide added functionalities (extensions in Eclipse terms). Extensions and extension points are defined in XML configuration files bundled with the plugins.

While the plugin paradigm leverages important patterns such as Separation of Concern, the strong interconnection and communication that plugins need can lead to wired dependencies between them. Typical examples arise from the need to locate singleton services available to the application, such as the database connection pool, the logging handles, or the user-saved preferences. Inversion of Control and Dependency Injection are viable solutions to remove such dependencies.

Inversion of Control and Dependency Injection

Inversion of Control is a programming pattern that focuses on the separation between how services (or application components) are defined and how they should locate the other services they depend on. To achieve such separation, implementations usually depend on a container, or locator framework, which is responsible for various tasks:

Existing frameworks actually use a combination of three basic techniques to perform the binding between services and components:

Head First Design Patterns

Related Reading

Head First Design Patterns
By Eric Freeman, Elisabeth Robson, Kathy Sierra, Bert Bates

We will adopt a variation of type 2, providing services via annotated methods. (The source code for the following sample application is available in the Resources section.) One way to declare a dependency can be expressed as:

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

  // save s1 and s2 into class variables
  // to use them when needed
}

The Inversion of Control container will look up the Injected annotation and invoke the method with the required parameters. In our effort to bring IoC into the Eclipse platform, the code that performs the binding between services and serviceable objects will be packaged as an Eclipse plugin. The plugin defines an extension point (named com.onjava.servicelocator.servicefactory) that can be used to contribute service factories to the application. The plugin will request service instances to a factory whenever a serviceable object needs to be configured. The ServiceLocator class will do all the work, as described in the following snippet (we will skip the code that handles the parsing of the extension points since it is quite straightforward):

/**
 * Injects the requested dependencies into the 
 * parameter object. It scans the serviceable 
 * object looking for methods tagged with the 
 * {@link Injected} annotation.Parameter types are 
 * extracted from the matching method. An instance
 * of each type is created from the registered 
 * factories (see {@link IServiceFactory}). When
 * instances for all the parameter types have been
 * created the method is invoked and the next one 
 * is examined.  
 *  
 * @param serviceable the object to be serviced
 * @throws ServiceException
 */
public static void service(Object serviceable)
  throws ServiceException {
        
  ServiceLocator sl = getInstance();
    if (sl.isAlreadyServiced(serviceable))    {    
    // prevent multiple initializations due to 
    // constructor hierarchies
   System.out.println(
      "Object " +
      serviceable + 
      " has already been configured ");
        return;
    }
        
    System.out.println("Configuring " + 
        serviceable);

    // Parse the class for the requested services
    for (Method m : 
        serviceable.getClass().getMethods()) {

    boolean skip=false;
    Injected ann=m.getAnnotation(Injected.class);
        if (ann != null) {                
            Object[] services = 
                new Object[m.getParameterTypes().length];
            int i = 0;
                
        for(Class<?> klass :m.getParameterTypes()){
        IServiceFactory factory = 
          sl.getFactory(klass,ann.optional());
                if (factory == null) {
                    skip = true;
                    break;
                }
          Object service =  
          factory.getServiceInstance();
                    
        // sanity check: verify that the returned
        // service's class is the expected one
        // from the method
                assert(service.getClass().equals(klass) || 
                  klass.isAssignableFrom(service.getClass()));
                    
                services[i++]  = service ;
       }
            
        try {
            if (!skip) 
                m.invoke(serviceable, services);
        }
        catch(IllegalAccessException iae) {
        if (!ann.optional())
               throw new ServiceException(
          "Unable to initialize services on " +
          serviceable + 
          ": " + iae.getMessage(),iae);
            }
            catch(InvocationTargetException ite) {
                if (!ann.optional())
                    throw new ServiceException(
            "Unable to initialize services on " + 
            serviceable + 
            ": " + ite.getMessage(),ite);
            }
        }
    }
        
    sl.setAsServiced(serviceable);
}

Since the services returned by the service factories may also be serviceable, this strategy allows the definition of service hierarchies (however, circular dependencies are not supported at the moment).

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:

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.

A Sample Eclipse RCP Application

We now have all the elements to build the application. Our sample application will be used to show the user interesting epigrams and citations, such as the fortune cookies. It will be composed of four plugins:

The IoC design we adopted keeps the service implementation separated from the client; the service implementation can now be changed or modified, leaving the client unaffected. Figure 2 shows the dependencies between plugins.

The dependencies between plugins
Figure 2. The dependencies between plugins: the ServiceLocator and the interface definition keep the service and the client separated.

As described in the previous sections, the Service Locator will bind together the client and the service in order to show the epigrams to the user. The FortuneInterface only defines the public interface IFortuneCookie, which clients can use to access the cookie message:

public interface IFortuneCookie {
        
        public String getMessage();

}

The FortuneService plugin provides a simple service factory that creates implementations of IFortuneCookie:

public class FortuneServiceFactory 
  implements IServiceFactory {

    public Object getServiceInstance() 
    throws ServiceException {

        return new FortuneCookieImpl();
    }

  // ... omissis ...

}

The factory is registered to the service locator plugin as an Eclipse extension, as described by its plugin.xml descriptor file:

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.0"?>
<plugin>
<extension
  point="com.onjava.servicelocator.servicefactory">
  <serviceFactory
    class="com.onjava.fortuneservice.FortuneServiceFactory"
    id="com.onjava.fortuneservice.FortuneServiceFactory"
    name="Fortune Service Factory"
    resourceClass="com.onjava.fortuneservice.IFortuneCookie"/>
</extension>

</plugin>

Here, the resourceClass attribute defines the class of the services provided by this factory. The described service is used by the Eclipse view defined in the FortuneClient plugin:

@Serviceable
public class View 
  extends ViewPart {

    public static final String ID = 
    "FortuneClient.view";
    
    private IFortuneCookie cookie;

    @Injected(optional=false) 
  public void setDate(IFortuneCookie cookie) {
        this.cookie = cookie;
    }

    public void createPartControl(Composite parent){
        Label l = new Label(parent,SWT.WRAP);
        l.setText("Your fortune cookie is:\n"
      + cookie.getMessage());
    }

    public void setFocus() {}
}

Notice the presence of the Serviceable and Injected annotations to define the dependencies on external services, and the absence of any reference to the code that provides the service. The final result is that createPartControl() is free to use the cookie object with the guarantee that it has been properly initialized. The sample application is shown in Figure 3.

The sample application
Figure 3. The sample application with a fortune cookie retrieved from the service plugin

Conclusion

In this article I have discussed how to merge a powerful programming pattern that simplifies the handling of code dependencies (Inversion of Control) with an emerging technology that speeds up the development of Java client applications (Eclipse RCP). Even if I have not dealt with the many details that affect the problem, I have demonstrated a sample application where services and service clients are decoupled. I have also described how the Eclipse plugin technology can preserve the separation of concerns when developing both clients and services. However, there are lot of interesting elements still to explore, such as the cleanup strategies to use when the services are no longer needed, or the ways to perform unit testing on our client plugins using mock-up services, which I'll leave to the reader.

Resources

Riccardo Govoni has been working since 2003 as a J2EE developer for a financial services company in the northern part of Italy.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.