控制 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檔案與外掛程式捆綁在一起。
外掛程式模式加強了關注分離的概念,外掛程式間的強串連和通訊需要通過配線進行設定它們之間的依賴。典型的例子源自需要定位應用程式所需要的單子服務,例如資料庫連接池、Tlog或使用者儲存的喜好設定。反轉控制和依賴注入是消除這種依賴的可行解決方案。
反轉控制和依賴注入
反轉控制是一種編程模式,它關注服務(或應用程式組件)是如何定義的以及他們應該如何定位他們依賴的其它服務。通常,通過一個容器或定位架構來獲得定義和定位的分離,容器或定位架構負責:
- 儲存可用服務的集合
- 提供一種方式將各種組件與它們依賴的服務綁定在一起
- 為應用程式代碼提供一種方式來請求已配置的對象(例如,一個所有依賴都滿足的對象), 這種方式可以確保該對象需要的所有相關的服務都可用。
現有的架構實際上使用以下三種基本技術的架構執行服務和組件間的綁定:
- 類型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。我們希望定義可以協作的外掛程式,但不需要顯示地引用它的基代碼。
出於這些原因,我將引入java轉換代理,它定義在 java.lang.instrument 包中,J2SE 5.0及更高版本支援。一個轉換代理是一個實現了 java.lang.instrument.ClassFileTransformer介面的對象,該介面只定義了一個 transform()方法。當一個轉換執行個體註冊到JVM時,每當JVM建立一個類的對象時都會調用它。這個轉換器可以訪問類的位元組碼,在它被JVM載入之前可以修改類的表示形式。
可以使用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視圖中顯示名言警句。
採用IoC設計,使服務的實現與客戶分離;服務執行個體可以修改,對客戶沒有影響。圖2顯示了外掛程式間的依賴關係。
圖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:\n" + cookie.getMessage()); } public void setFocus() { } }
注意這裡出現了Serviceable和Injected注釋,用於定義依賴的外部服務,並且沒有引用任何服務代碼。最終結果是,createPartControl() 可以自由地使用cookie對象,可以確保它被正確地初始化。樣本程式如圖3所示
圖3. 樣本程式
結論
本文我討論了如何結合使用一個強大的編程模式--它簡化了代碼依賴的處理(反轉控制),與Java用戶端程式(Eclipse RCP)。即使我沒有處理影響這個問題的更多細節,我已經示範了一個簡單的應用程式的服務和客戶是如何解耦的。我還描述了當開發客戶和服務時, Eclipse外掛程式技術是如何?關注分離的。然而,還有許多有趣的因素仍然需要去探究,例如,當服務不再需要時的清理策略,或使用mock-up服務對用戶端外掛程式進行單元測試,這些問題我將留給讀者去思考。