在Eclipse RCP中实现控制反转(IoC)

来源:互联网 发布:儿童学围棋软件 编辑:程序博客网 时间:2024/05/01 17:36

作者:Riccardo Govoni;shenpipi

Eclipse胖客户端平台(Rich Client Platform,RCP)是一个功能强大的软件基础(software foundation),它基于相互联系并协作的插件,允许开发人员创建通用的应用程序。RCP使得开发人员可以更加关注应用程序的业务逻辑而不是花大量时间来重新发明轮子去编写大量的应用程序管理逻辑。

控制翻转(IOC)和依赖注射是一种减少程序耦合性的设计模式。它们遵循了一个简单的原则:你不必自己创建对象,你只需要描述如何创建对象。你不必去实例化或者定位你的组件需要的服务,你只需要去描述哪个组件需要哪个服务,其他组件(通常是容器)来负责将它组装好。这也就是著名的好莱坞原则:不要打电话给我们,我们会打给你。

这篇文章描述了在Eclipse RCP中引入依赖注射机制的一个简单方法。为了避免污染Eclipse平台的基础设施并且透明的为RCP添加IoC框架,我们使用了动态字节码操作(使用ObjectWeb ASM类库),Java类加载agent以及Java标注技术的组合。

 什么是Eclipse胖客户端平台?

简单来说,Eclipse胖客户端平台是一组类库,软件框架以及运行环境,它可以用来创建独立运行并且经常需要与网络交互的应用程序。

尽管Eclipse是作为一个集成开发环境(IDE)框架设计的,但是,从3.0版本开始,整个项目已经被重构成各种独立的组件,以便于可以使用这些组件的一个子集来构建任意的应用程序。这个子集构成了RCP,它包含以下几种组件:基本运行环境,用户接口组件(SWT和JFace),插件以及OSGi层。图1展示了Eclipse平台的主要组件。

image
图1. Eclipse平台的主要组件

整个Eclipse平台是基于插件和扩展点(extensions points)这样一个关键概念的。一个插件是可以独立开发和发布的功能的最小单元。它一般会打包成一个jar文件并且通过增加功能来扩充Eclipse平台(例如,增加一个编辑器,工具栏按钮或者一个编译器)。整个平台就是一组互相联系并通讯的一组插件的集合。一个扩展点是一个已经存在的互相联系的端点,可以被其他插件用来添加功能(功能:在Eclipse术语中叫做扩展)。扩展和扩展点通过XML配置文件定义并绑定在插件中。

尽管插件已经使用了关注点分离这样一个重要的模式,但是插件之间的强相互联系和通讯会导致他们之间的强依赖关系。一个典型的例子就是需要定位应用程序需要的各种单例(Singleton)服务,例如数据库连接池,日志处理器,或者用户偏爱(preference)的设置信息的保存等。控制反转和依赖注射是消除这些依赖的可行解决方案。

控制反转和依赖注射
+控制反转是一种设计模式,它主要关注如何将服务(或者应用程序组件)的定义和服务如何定位它们依赖的服务进行分离。为了完成这种分离,一般都会依赖一个容器,或者定位框架(locator framework),维护现存服务的列表
+提供一种方法来将组件和它们依赖的服务绑定在一起。
+提供一种方法让应用程序代码可以请求一个已经配置好的对象(例如,一个所有依赖都已经满足的对象),这样就可以保证该对象所有相关的服务都已经可用了。

现存的框架一般都使用下面三种基本技术的组合来绑定服务和组件:
+类型一(基于接口):服务对象需要实现一个专门的接口,这个接口为这些服务对象提供了一个对象,服务可以通过这个对象来查询它们的依赖。这是一些早期的容器使用的模式,例如Excalibur。
+类型二(基于setter):通过JavaBean属性的setter方法将依赖的服务赋值给服务对象。HiveMind和Spring都是通过这种方式来实现的。
+类型三(基于构造函数):依赖的服务通过构造函数的参数提供(不通过JavaBean属性暴露)。这是PicoContainer使用的唯一方式。HiveMind和Spring也使用了这种方式。
我们将采用第二种方式的变种,通过带标注的方法来提供服务。(示例应用的源代码在资源中可以找到)。声明依赖可以采用以下方式:

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

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



控制反转容器会查找Injected标注并且使用需要的参数来调用这个方法。在我们为Eclipse平台引入IoC的过程中,在服务和可服务对象间建立绑定关系的代码被封装在一个Eclipse插件中。这个插件定义了一个扩展点(com.onjava.servicelocator.servicefactory),用来为应用程序提供服务工厂。当一个可服务对象需要配置时,插件会向工厂请求服务的实例。正如下面的代码,ServiceLocator类将会完成所有的工作。(我们会跳过那些处理扩展点解析的代码,因为这些代码会很简单)

/**
* 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);
}



既然这个服务工厂返回的服务同样可以是一个可服务对象,这个策略允许定义一种服务层次(但是,目前并不提供对循环依赖的支持)。

ASM and java.lang.instrument Agents

以上介绍的各种注射策略都依赖于容器的存在,依赖容器提供一个入口点来使得应用程序可以使用该入口点来请求已经得到正确配置的对象。但是,我们想在开发我们的IoC插件的时候使用一种透明的方式,这主要有两个原因:
+RCP采用了复杂的classloader以及初始化策略(考虑一下createExecutableExtension())来维护插件的隔离性以及可见性的限制。我们不想修改或者替代这种策略来引入我们的基于容器的初始化规则。
+对这样一个入口点(这个例子中,是service locator插件中定义的service()方法)的显式引用将会强迫应用程序开发人员采用一种显式的模式和逻辑来获取初始化的组件。这样就会有一些应用-锁定的类库出现在程序代码中。我们想要定义一个合作的插件,这个插件并不需要显示的引用这些代码。

出于以上原因,我们要引入定义在java.lang.instrument包中的Java转换代理,这个包出现在J2SE5.0以及以后的版本中。一个转换代理是一个实现了java.lang.instrument.ClassFileTransformer接口的对象,这个接口只有唯一一个方法,transform()。当一个转换器的实例注册到JVM中后,当JVM要创建任何类的时候,这个代理的transform()会被调用。转换器可以在JVM加载类之前访问这个类的字节码并且可以对其进行修改。

转换代理可以采用-javaagent:jarpath[=options]的形式的命令行参数来注册到JVM,其中jarpath是包含了这个代理类的JAR文件,options是传递给该代理的一个参数字符串。这个代理JAR文件使用一个特殊的MANIFEST属性来指明真正的代理类,这个代理类必须定义一个public static void premain(String options, Instrumentation inst)方法。这个premain()方法将会在应用程序的main方法被调用之前调用,并且将一个真正的变换器注册到传递进来的java.lang.instrument.Instrumentation类的实例中。

在我们的示例程序中,我们定义一个代理来透明的进行字节码操作并且动态的调用我们的IoC容器(service vlocator插件)。这个代理会通过检验是否存在Serviceable标注来确定一个可服务对象。然后,修改所有的构造函数来调用IoC容器的方法在对象初始化时正确地配置和初始化这个对象。

让我们假设我们有一个需要依赖于其他服务的对象(还记得Injected标注吗?):

@Serviceable
public class ServiceableObject {
  
  public ServiceableObject() {

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

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

    // ... omissis ...
  }

}



当它被转换代理操作以后,它的字节码会和下面这个进行正常编译的类的字节码一样:

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

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

    // ... omissis ...
  }

}



通过这种解决方案,我们不必将对容器的依赖进行硬编码就可以配置一个可服务对象并且使他们可用。开发人员仅仅需要在可服务对象类上使用Serviceable标注就可以了,变换代理的代码如下:

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());
    }

}



其中ConstructorVisitor, ClassAnnotationVisitor, ClassWriter以及 ClassConstructorWriter这几个类使用ObjectWeb ASM类库来进行字节码操作。

ASM使用访问者模式来将类数据(包括指令序列)作为事件流处理。当解码一个已经存在的类时,ASM为我们产生事件流,调用我们的方法来处理事件。当生成一个新类的时候,采用相反的过程:我们产生一个事件流,ASM类库将它转化成一个类。注意我们描述的这个方法并不依赖于特定的字节码类库(我们这里用了ASM),其他的解决方案,例如BCEL和Javassist同样可以工作。
我们不想深究ASM的内部机制。对于本文的目的来说,知道ConstructorVisitor和ClassAnnotationVisitor对象是用来确定使用了Serviceable标注的类并且收集他们的构造函数就足够了。它们的代码如下:
        

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);
    }
}



对于上述类收集到的每一个构造函数,使用一个ClassConstructorWriter类的实例来修改构造函数的字节码,注入对service locator插件的调用:

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



采用ASM方式完成上述工作需要以下代码:

// 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");



第一个指令将第二个指令需要使用的this对象的引用放在堆栈上,第二个指令将调用ServiceLocator的一个静态方法。
一个示例Eclipse RCP应用
我们现在已经具有了所有创建应用程序的元素,我们的示例代码用来向用户显示有趣的警句和引用,就像fortune cookies一样。它包含了四个插件:
+service locator插件,提供Ioc框架的功能
+FortuneService插件,提供了管理fortune cookies的服务。
+FortuneInterface插件,发布访问服务需要的公共接口。
+客户端插件,作为Eclipse应用在一个Eclipse视图中显示格式化的警句。

我们采用的IoC设计使得服务的实现和客户端分离,服务的实现可以改变或者修改,而客户端却不受影响。图2显示了插件之间的依赖关系。

image
图2. 插件之间的依赖关系,ServiceLocator和接口的定义使得服务和客户分离开。

就像上面几节描述的那样,为了向用户显示警句,service locator将会将客户和服务绑定在一起。FortuneInterface仅仅定义一个公有的接口,用户可以使用它来访问cookie消息。

public interface IFortuneCookie {
        
        public String getMessage();

}



Fortune插件提供了一个简单的服务工厂来创建IFortuneCookie的实现类的实例。

public class FortuneServiceFactory 
  implements IServiceFactory {

    public Object getServiceInstance()
    throws ServiceException {

        return new FortuneCookieImpl();
    }

  // ... omissis ...

}



工厂作为一个Eclipse扩展注册到service locator,就像它的plugin.xml中描述的一样。

<?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>



这里,resourceClass属性定义了这个工厂提供的服务类。描述的服务被FortuneClient插件中的Eclipse视图使用。

@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() {}
}



注意要加上Serviceable和Injected标注来定义对其他服务的依赖,但是不需要引用提供这些服务的代码。最后的结果是createPartControl()可以自由得使用cookie对象而且可以保证cookie是正确初始化过的。示例应用程序如图3所示。

image
图3 示例应用,从服务插件获取一个fortune cookie

结论

在这篇文章中,我们讨论了如何将一种强大的设计模式(可以简化代码依赖的处理,IoC)与一种可以加速Java客户端应用程序开发的技术合并在一起。虽然我没有处理更多于这个问题有关的细节,但是我也已经演示了一个示例应用如何将服务和服务客户解耦。我同时也描述了Eclipse插件技术在开发客户端和服务时如何做到了关注点分离。但是,仍然有很多有趣的元素在等待我们探索,例如清除策略,用来在不需要这个服务的时候清除掉服务,或者在我们的客户插件中使用mock-up服务来进行单元测试,这些要留给读者去研究了。

Resources
+本文的示例代码
+Eclipse.org网站上的Eclipse RCP教程
+ASM网站上的ASM 2.0介绍