在EclipseRCP中實現反轉控制(IoC)
Eclipse富客戶平臺(RCP)是一個功能強大的軟件平臺,它基于插件間的互連與協作,允許開發人員構建通用的應用程序。RCP使開發人員可以集中精力進行應用程序業務代碼的開發,而不需要花費時間重新發明輪子編寫應用程序管理的邏輯。
反轉控制(Inversion of Control, IoC)和依賴注入(Dependency Injection, DI)是兩種編程模式,可用于減少程序間的耦合。它們遵循一個簡單的原則:你不要創建你的對象;你描述它們應當如何被創建。你不要實例化你的部件所需要對象或直接定位你的部件所需要的服務;相反,你描述哪個部件需要哪些服務,其它人(通常是一個容器)負責將它們連接到一起。這也被認為是好萊塢法則:don't call us--we'll call you。
本文將描述一個簡單的方式在Eclipse RCP應用程序中使用依賴注入。為了避免污染Eclipse 平臺的基礎結構以及透明地在RCP之上添加IoC框架,我們將結合使用運行時字節碼操作技術(使用 ObjectWeb ASM庫)、Java類加載代理(使用java.lang.instrument包)以及Java annotation。
什么是Eclipse富客戶平臺?
用一句話來講,富客戶平臺是一個類庫、軟件框架的集合,它是一個用于構建單機和連網應用程序的運行時環境。
盡管Eclipse被認為是構建集成開發環境(IDE)的框架,從3.0開始,Eclipse整個產品進行了重構,分割成各種不同的部件,它些部件可以用于構建任意的應用程序。其中的一個子集構成了富客戶平臺,它包含以下元素:基本的運行時環境、用戶界面組件(SWT和JFace)、插件以及 OSGI層。圖1顯示了Eclipse平臺的主要部件。

圖1. Eclipse平臺的主要部件
整個Eclipse平臺是基于插件和擴展點。一個插件是一個可以獨立開發和發布的最小的功能單元。它通常打包成一個jar文件,通過添加功能(例如,一個編輯器、一個工具欄按鈕、或一個編譯器)來擴展平臺。整個平臺是一個相互連接和通信的插件的集合。一個擴展點是一個互相連接的端點,其它插件可以用它提供額外的功能(在Eclipse中稱為擴展)。擴展和擴展點定義在XML配置文件中,XML文件與插件捆綁在一起。
插件模式加強了關注分離的概念,插件間的強連接和通訊需要通過配線進行設置它們之間的依賴。典型的例子源自需要定位應用程序所需要的單子服務,例如數據庫連接池、日志處理或用戶保存的首選項。反轉控制和依賴注入是消除這種依賴的可行解決方案。
反轉控制和依賴注入
反轉控制是一種編程模式,它關注服務(或應用程序部件)是如何定義的以及他們應該如何定位他們依賴的其它服務。通常,通過一個容器或定位框架來獲得定義和定位的分離,容器或定位框架負責:
由于服務工廠返回的服務可能也是可服務對象,這種策略允許定義服務的層次結構(然而目前不支持循環依賴)。
ASM和java.lang.instrument代理
前節所述的各種注入策略通常依靠容器提供一個入口點,應用程序使用入口點請求已正確配置的對象。然而,我們希望當開發IoC插件時采用一種透明的方式,原因有二:
可以使用JVM命令行參數注冊轉換代理,形式為-javaagent:jarpath[=options],其中jarpath是包含代碼類的JAR文件的路徑, options是代理的參數字符串。代理JAR文件使用一個特殊的manifest屬性指定實際的代理類,該類必須定義一個 public static void premain(String options, Instrumentation inst)方法。代理的premain()方法將在應用程序的main()執行之前被調用,并且可以通過傳入的java.lang.instrument.Instrumentation對象實例注冊一個轉換器。
在我們的例子中,我們定義一個代理執行字節碼操作,透明地添加對Ioc容器(Service Locator 插件)的調用。代理根據是否出現Serviceable注釋來標識可服務的對象。接著它將修改所有的構造函數,添加對IoC容器的回調,這樣就可以在實例化時配置和初始化對象。
假設我們有一個對象依賴于外部服務(Injected注釋):
ASM使用visitor模式以事件流的方式處理類數據(包括指令序列)。當解碼一個已有的類時, ASM為我們生成一個事件流,調用我們的方法來處理這些事件。當生成一個新類時,過程相反:我們生成一個事件流,ASM庫將其轉換成一個類。注意,這里描述的方法不依賴于特定的字節碼庫(這里我們使用的是ASM);其它的解決方法,例如BCEL或Javassist也是這樣工作的。
我們不再深入研究ASM的內部結構。知道ConstructorVisitor和 ClassAnnotationVisitor對象用于查找標記為Serviceable類,并收集它們的構造函數已經足夠了。他們的源代碼如下:
Eclipse RCP應用程序示例
現在我們具有了構建應用程序的所有元素。我們的例子可用于顯示用戶感興趣的名言警句。它由四個插件組成:

圖2. 插件間的依賴關系: ServiceLocator和接口定義使服務和客戶分離。
如前面所述,Service Locator將客戶和服務綁定到一起。FortuneInterface只定義了公共接口 IFortuneCookie,客戶可以用它訪問cookie消息:

圖3. 示例程序
結論
本文我討論了如何結合使用一個強大的編程模式--它簡化了代碼依賴的處理(反轉控制),與Java客戶端程序(Eclipse RCP)。即使我沒有處理影響這個問題的更多細節,我已經演示了一個簡單的應用程序的服務和客戶是如何解耦的。我還描述了當開發客戶和服務時, Eclipse插件技術是如何實現關注分離的。然而,還有許多有趣的因素仍然需要去探究,例如,當服務不再需要時的清理策略,或使用mock-up服務對客戶端插件進行單元測試,這些問題我將留給讀者去思考。
反轉控制(Inversion of Control, IoC)和依賴注入(Dependency Injection, DI)是兩種編程模式,可用于減少程序間的耦合。它們遵循一個簡單的原則:你不要創建你的對象;你描述它們應當如何被創建。你不要實例化你的部件所需要對象或直接定位你的部件所需要的服務;相反,你描述哪個部件需要哪些服務,其它人(通常是一個容器)負責將它們連接到一起。這也被認為是好萊塢法則:don't call us--we'll call you。
本文將描述一個簡單的方式在Eclipse RCP應用程序中使用依賴注入。為了避免污染Eclipse 平臺的基礎結構以及透明地在RCP之上添加IoC框架,我們將結合使用運行時字節碼操作技術(使用 ObjectWeb ASM庫)、Java類加載代理(使用java.lang.instrument包)以及Java annotation。
什么是Eclipse富客戶平臺?
用一句話來講,富客戶平臺是一個類庫、軟件框架的集合,它是一個用于構建單機和連網應用程序的運行時環境。
盡管Eclipse被認為是構建集成開發環境(IDE)的框架,從3.0開始,Eclipse整個產品進行了重構,分割成各種不同的部件,它些部件可以用于構建任意的應用程序。其中的一個子集構成了富客戶平臺,它包含以下元素:基本的運行時環境、用戶界面組件(SWT和JFace)、插件以及 OSGI層。圖1顯示了Eclipse平臺的主要部件。

圖1. Eclipse平臺的主要部件
整個Eclipse平臺是基于插件和擴展點。一個插件是一個可以獨立開發和發布的最小的功能單元。它通常打包成一個jar文件,通過添加功能(例如,一個編輯器、一個工具欄按鈕、或一個編譯器)來擴展平臺。整個平臺是一個相互連接和通信的插件的集合。一個擴展點是一個互相連接的端點,其它插件可以用它提供額外的功能(在Eclipse中稱為擴展)。擴展和擴展點定義在XML配置文件中,XML文件與插件捆綁在一起。
插件模式加強了關注分離的概念,插件間的強連接和通訊需要通過配線進行設置它們之間的依賴。典型的例子源自需要定位應用程序所需要的單子服務,例如數據庫連接池、日志處理或用戶保存的首選項。反轉控制和依賴注入是消除這種依賴的可行解決方案。
反轉控制和依賴注入
反轉控制是一種編程模式,它關注服務(或應用程序部件)是如何定義的以及他們應該如何定位他們依賴的其它服務。通常,通過一個容器或定位框架來獲得定義和定位的分離,容器或定位框架負責:
- 保存可用服務的集合
- 提供一種方式將各種部件與它們依賴的服務綁定在一起
- 為應用程序代碼提供一種方式來請求已配置的對象(例如,一個所有依賴都滿足的對象), 這種方式可以確保該對象需要的所有相關的服務都可用。
- 類型1 (基于接口): 可服務的對象需要實現一個專門的接口,該接口提供了一個對象,可以從用這個對象查找依賴(其它服務)。早期的容器Excalibur使用這種模式。
- 類型2 (基于setter): 通過JavaBean的屬性(setter方法)為可服務對象指定服務。HiveMind和Spring采用這種方式。
- 類型3 (基于構造函數): 通過構造函數的參數為可服務對象指定服務。PicoContainer只使用這種方式。HiveMind和Spring也使用這種方式。
@Injected public void aServicingMethod(Service s1, AnotherService s2) { // 將s1和s2保存到類變量,需要時可以使用 }反轉控制容器將查找Injected注釋,使用請求的參數調用該方法。我們想將IoC引入Eclipse平臺,服務和可服務對象將打包放入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<?> class : m.getParameterTypes()) { IServiceFactory factory = sl.getFactory(class, 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(class) || class .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和java.lang.instrument代理
前節所述的各種注入策略通常依靠容器提供一個入口點,應用程序使用入口點請求已正確配置的對象。然而,我們希望當開發IoC插件時采用一種透明的方式,原因有二:
- RCP采用了復雜的類加載器和實例化策略(想一下createExecutableExtension()) 來維護插件的隔離和強制可見性限制。我們不希望修改或替換這些策略而引入我們的基于容器的實例化規則。
- 顯式地引用這樣一個入口點(Service Locator插件中定義的service()方法) 將強迫應用程序采用一種顯式地模式和邏輯來獲取已初始化的部件。這表示應用程序代碼出現了library lock-in。我們希望定義可以協作的插件,但不需要顯示地引用它的基代碼。
可以使用JVM命令行參數注冊轉換代理,形式為-javaagent:jarpath[=options],其中jarpath是包含代碼類的JAR文件的路徑, options是代理的參數字符串。代理JAR文件使用一個特殊的manifest屬性指定實際的代理類,該類必須定義一個 public static void premain(String options, Instrumentation inst)方法。代理的premain()方法將在應用程序的main()執行之前被調用,并且可以通過傳入的java.lang.instrument.Instrumentation對象實例注冊一個轉換器。
在我們的例子中,我們定義一個代理執行字節碼操作,透明地添加對Ioc容器(Service Locator 插件)的調用。代理根據是否出現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使用visitor模式以事件流的方式處理類數據(包括指令序列)。當解碼一個已有的類時, 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應用程序示例
現在我們具有了構建應用程序的所有元素。我們的例子可用于顯示用戶感興趣的名言警句。它由四個插件組成:
- Service Locator插件,提供IoC框架
- FortuneService插件,提供服務管理fortune cookie
- FortuneInterface插件,發布訪問服務所需的公共接口
- FortuneClient插件,提供Eclipse應用程序,以Eclipse視圖中顯示名言警句。

圖2. 插件間的依賴關系: ServiceLocator和接口定義使服務和客戶分離。
如前面所述,Service Locator將客戶和服務綁定到一起。FortuneInterface只定義了公共接口 IFortuneCookie,客戶可以用它訪問cookie消息:
public interface IFortuneCookie { public String getMessage(); }FortuneService提供了一個簡單的服務工廠,用于創建IFortuneCookie的實現:public class FortuneServiceFactory implements IServiceFactory { public Object getServiceInstance() throws ServiceException { return new FortuneCookieImpl(); } // ... omissis ... }工廠注冊到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:" + cookie.getMessage()); } public void setFocus() { } }注意這里出現了Serviceable和Injected注釋,用于定義依賴的外部服務,并且沒有引用任何服務代碼。最終結果是,createPartControl() 可以自由地使用cookie對象,可以確保它被正確地初始化。示例程序如圖3所示
圖3. 示例程序
結論
本文我討論了如何結合使用一個強大的編程模式--它簡化了代碼依賴的處理(反轉控制),與Java客戶端程序(Eclipse RCP)。即使我沒有處理影響這個問題的更多細節,我已經演示了一個簡單的應用程序的服務和客戶是如何解耦的。我還描述了當開發客戶和服務時, Eclipse插件技術是如何實現關注分離的。然而,還有許多有趣的因素仍然需要去探究,例如,當服務不再需要時的清理策略,或使用mock-up服務對客戶端插件進行單元測試,這些問題我將留給讀者去思考。