`

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

    博客分类:
  • j2se
 
阅读更多

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

    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平台的主要组件。

    整个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标注吗?):@Serviceablepublic class ServiceableObject {    public ServiceableObject() {    System.out.println("Initializing……");  }  @Injected public void aServicingMethod(    Service s1,     AnotherService s2) {    // …… omissis ……  }}

    当它被转换代理操作以后,它的字节码会和下面这个进行正常编译的类的字节码一样:@Serviceablepublic 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 manipulationmv.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设计使得服务的实现和客户端分离,服务的实现可以改变或者修改,而客户端却不受影响。

    就像上面几节描述的那样,为了向用户显示警句,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视图使用。

    @Serviceablepublic 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是正确初始化过的。

    结论

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

    Resources +本文的示例代码+Eclipse.org网站上的Eclipse RCP教程+ASM网站上的ASM 2.0介绍+Matrix,与Java共舞+http://www.onjava.com
分享到:
评论

相关推荐

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

    在Eclipse RCP中实现控制反转(IoC)是一种提高应用程序可维护性和可扩展性的重要设计策略。控制反转(Inversion of Control,IoC)和依赖注射(Dependency Injection,DI)是面向对象编程中降低模块间耦合的技术,...

    Eclipse rcp深入浅出中英文版及配套代码

    10. **调试和测试**:书中还会介绍如何在Eclipse RCP环境中进行调试和编写单元测试。 11. **扩展点(Extension Points)**:这是Eclipse RCP插件系统的关键特性,允许插件之间通过定义和使用扩展点来相互协作。 12...

    Eclipse RCP 软件打包发布方法

    1. **新建“产品配置”**:在Eclipse中,你需要通过"File" -&gt; "New" -&gt; "Other" -&gt; "Plug-in Development" -&gt; "Product Configuration"来创建一个新的产品配置。这个配置定义了你的应用程序的基本属性,如应用程序的...

    在EclipseRCP中实现反转控制(IoC)

    反转控制(InversionofControl,IoC)和依赖注入(DependencyInjection,DI)是两种编程模式,可用于减少程序间的耦合。它们遵循一个简单的原则:你不要创建你的对象;你描述它们应当如何被创建。你不要实例化你的部件所...

    Eclipse RCP中使用第三方包

    然而,在实际开发中,我们经常需要使用第三方包来实现某些功能,这篇文章将介绍如何在Eclipse RCP中使用第三方包。 首先,我们需要新建一个Eclipse RCP应用程序,然后创建一个lib目录作为存放第三方库的目录。在这...

    Eclipse RCP(富客户端平台)开发中文语言包_3.6.0.rar

    在Eclipse RCP中,国际化是通过资源bundle实现的,这些bundle包含特定语言的字符串和其他UI资源。开发者需要创建对应语言的.properties文件,并在代码中使用ResourceBundle来获取本地化的字符串。 8. **调试和测试...

    eclipse rcp应用系统开发方法与实战源代码.zip

    在"eclipse rcp应用系统开发方法与实战源代码.zip"中,我们可以学习到以下关键知识点: 1. **Eclipse RCP架构**:理解Eclipse RCP的基础架构非常重要,包括插件(Plugins)、工作台(Workbench)、视图(Views)、...

    EclipseRCP教程

    在本教程中,我们将详细介绍 Eclipse RCP 的开发过程、技术要点和注意事项,以帮助开发者快速掌握 Eclipse RCP 的开发技术。 一、Eclipse RCP 的技术要点 Eclipse RCP 的核心技术包括: 1. SWT(Standard Widget ...

    eclipse RCP Plug-in开发自学教程.pdf

    RCP插件式开发方式可以重用eclipse中的方法和编码模式,提高开发效率和代码复用率。然而,Eclipse RCP的学习曲线可能较陡,需要一定的Java基础和Eclipse基础知识。 本教程旨在帮助读者自学Eclipse RCP插件式开发,...

    Eclipse RCP.pdf清晰版

    1. **创建新的Eclipse插件项目**: 在Eclipse中选择File -&gt; New -&gt; Other -&gt; Plug-in Project。 2. **定义插件元数据**: 描述插件的基本信息,如名称、ID、版本号等。 3. **实现功能**: 开发插件的核心逻辑和用户界面...

    ECLIPSE+RCP应用系统开发方法与实战(PDF 高岗著)

    5. **命令和服务**:Eclipse RCP中的命令(Command)和服务(Service)机制,用于实现应用的可扩展性和互操作性,读者将学习如何定义和使用这些组件。 6. **透视图和工作台**:透视图(Perspective)定义了工作空间...

    在Eclipse RCP中应用Spring OSGI 管理bean(一)

    标题中的“在Eclipse RCP中应用Spring OSGI 管理bean(一)”表明这是一篇关于如何在Eclipse Rich Client Platform (RCP)应用程序中集成Spring框架,并利用OSGi服务来管理Bean的教程。Eclipse RCP是一个用于构建桌面...

    Eclipse Rcp

    虽然SWT/JFace开发基础知识是可选的,但是这些是Eclipse RCP开发中常用的图形用户界面技术,熟悉它们会对开发有所帮助。 Eclipse RCP应用的开发过程需要利用Eclipse插件开发工具PDE(Plug-in Development ...

    eclipse RCP mp3工程

    【描述】中的“非常棒的一个rcp应用程序”意味着这个工程展示了Eclipse RCP的强大功能和易用性,可能是通过集成MP3播放、管理、编辑等功能来实现的。"学习学习,快来下"则提示这个项目适合学习Eclipse RCP的开发者,...

    EclipseRcp 例子程序

    在“Eclipse RCP 例子程序”中,我们可能找到一系列的示例代码和项目,这些示例展示了如何利用Eclipse RCP的各种组件和机制来构建实际的应用。以下是一些关键的知识点: 1. **插件系统**:Eclipse RCP的核心是其...

    Eclipse RCP培训.zip

    8. **模型-视图-控制器(MVC)**:Eclipse RCP遵循MVC设计模式,模型负责数据管理,视图负责显示,控制器处理用户输入和模型-视图间的交互。 9. **透视图(Perspective)**:透视图是工作台的一种布局,可以包含多...

    eclipse rcp check table

    在Eclipse RCP中实现这样的功能,主要步骤如下: 1. **创建表视图**:使用`SWT.Table`创建一个表格控件,然后设置其布局和大小。 2. **添加列**:通过`TableColumn`对象为表格添加列,可以指定列的标题和宽度。 3...

    Eclipse RCP 插件开发指南

    在 Eclipse RCP 中,JUnit 可以用来验证插件的行为是否符合预期。编写良好的单元测试有助于确保代码的质量和稳定性。 ##### JFace Data Binding JFace 数据绑定提供了将 UI 控件与模型对象自动同步的能力。通过...

    ECLIPSE RCP项目源程序

    1. **插件(Plugins)**:Eclipse RCP基于插件架构,每个功能模块都被封装在单独的插件中,这样可以实现模块化开发,提高代码的复用性和可扩展性。在标签中提到的"ECLIPSE RCP 插件",意味着这个项目可能包含多个...

Global site tag (gtag.js) - Google Analytics