一.引言
Eclipse富客戶端平台(RCP)是一個強有力的軟件基礎庫-它基於相互聯系的協作性插件,允許開發者構建普通應用程序。借助於RCP,開發者只需專注於應用程序業務代碼的開發而不必花時間去重寫應用程序管理邏輯。
控制反轉(IoC)和依賴性注入(DI)都是能夠用來減少程序之間的耦合度的編程模式。它們都遵循一種簡單的原則:你不必創建自己的對象,而只需描述該對象如何被創建;你不必實例化或直接定位你的組件需要的服務,而只需確定哪些服務為哪些組件所需要,然後由其它程序(通常是一個容器)負責把它們"鉤"到一起。這就是著名的"好萊塢原則"-不要找我們,讓我們找你好了。
本文將描述一種把依賴性注入支持功能加入到一個Eclipse RCP應用程序中的簡單方法。為了避免影響Eclipse平台基本結構並且為了把IoC框架透明地添加到RCP,我們將聯合使用運行時刻字節碼操作技術(使用ObjectWeb ASM庫),Java類加載代理(使用java.lang.instrument包)和Java注解技術。
二.什麼是Eclipse豐富客戶端平台?
用一句話來概括,Eclipse富客戶端平台就是用於構建既能獨立運行又能在網絡中運行的應用程序的一組庫,軟件框架及一個運行時刻環境。
盡管Eclipse被作為一種開發程序的IDE來使用;但是,整個軟件自從3.0版發行以來被重新構建成各種獨立的組件,以便可以使用這些組件的最小子集來構建任意的應用程序。這樣的一個子集構成了豐富客戶端平台並且包括不同的元素:基本運行時刻,用戶接口組件(SWT和JFace),插件,還有OSGi層。圖1展示了Eclipse平台的主要組件構成。
圖1.Eclipse平台的主要構成組件
整個Eclipse平台基於兩個關鍵概念-插件和擴展點。一個插件是一個小的功能單元,它能夠被獨立地開發和發布。典型情況下,插件被打包為一個jar文件,並且能夠通過添加某種功能來擴展Eclipse平台(例如,一個編輯器,一個工具欄按鈕或一個編譯器)。其實,整個Eclipse平台就是一組相互連接的彼此之間能夠進行通訊的插件。一個擴展點是一個可用的連接點,其它的插件可以用來提供添加的功能(用術語來說,就是"擴展")。擴展和擴展點都是在與插件綁定到一起的XML配置文件中定義的。
盡管插件機制利用了重要的模式-例如關系分離,強連接等等;但是,插件需要的通訊能夠導致這些插件間的物理依賴性。典型的例子就是,插件需要定位可用於應用程序的單例(singleton)服務-例如數據庫連接池,日志處理或用戶保存的收藏信息。控制反轉和依賴性注入都是去除這種依賴性的可行方案。
三.控制反轉和依賴性注入
控制反轉是一種編程模式-主要目的是實現服務(或應用程序組件)的定義方式與這些服務應該怎樣定位自己依賴的其它服務之間的分離。
為了實現分離,其實現通常依賴於一個容器或定位符框架-由它們來負責各種具體的任務:
保持一組可用的服務
提供一種方式把組件(即"可服務的對象")與其依賴的服務綁定到一起
為應用程序代碼提供一種方式以請求一個配置好的對象(也就是說,一個其依賴性都得到滿足的對象)-這可以確保所有相關的服務都可用於該對象
實際上,目前的框架基本都是使用三種基本技術的組合來實現服務與組件之間的綁定的:
類型1(基於接口):可服務的對象需要實現一個專用接口-由這個接口提供給這些對象一個它們能夠查找依賴性(其它服務)的對象。這是Excalibur所提供的早期容器使用的模式。
類型2(基於setter):服務被經由JavaBeans屬性(setter方法)賦給可服務的對象。例如,HiveMind和Spring都使用這種方法。
類型3(基於構造器):服務被提供為構造器參數(並不被暴露為JavaBeans屬性)。這是一種由PicoContainer專用的獨特的方法。另外,它也用於HiveMind和Spring中。
我們將采納類型2的一種變體-通過注解的方法提供服務。一種聲明一個依賴性的方法可以如下實現:
@Injected public void aServicingMethod(
Service s1,AnotherService s2) {
//把s1和s2保存到類變量中
//以便在需要時使用它們
}
控制反轉容器將查找注入的注解並且調用要求的參數來調用該方法。為了把IoC加入到Eclipse平台中,我們把在服務和可服務的對象之間進行綁定的代碼打包為一個Eclipse插件。該插件定義一個擴展點(名為com.onjava.servicelocator.servicefactory)-它可以用來為應用程序提供服務工廠。無論何時當一個可服務的對象需要配置時,該插件將請求到一個工廠的服務實例。ServiceLocator類負責實現所有這些工作,正如下面的代碼片斷所描述的(我們跳過處理分析擴展點的代碼-因為這些代碼非常直接):
/**
*把要求的依賴性注入到參數對象中。它掃描可服務的對象-通過查找標識有{@link Injected}注解的方法。參數類型是從匹配的方法中提取的。每一種類型的實例是從注冊的工廠中創建的(見{@link IServiceFactory})。相應於所有參數類型的實例都被創建完畢,該方法被調用,並繼續檢查下一個方法。
*
* @param-要被服務的可服務對象
* @拋出ServiceException異常
*/
public static void service(Object serviceable)
throws ServiceException {
ServiceLocator sl = getInstance();
if (sl.isAlreadyServiced(serviceable)) {
//避免多次初始化問題-由於存在構造器分層
System.out.println("Object " +serviceable + " has already been configured ");
return;
}
System.out.println("Configuring " + serviceable);
//為請求的服務分析類
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();
//檢查:確保返回的服務的類是從該方法中盼望的那一個
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和java.lang.instrument代理
前一節所描述的各種注入策略通常都依靠存在一個容器來提供一個空的入口點-應用程序用來請求正確配置的對象。然而,在開發我們的IoC插件時,我們想維護一種透明的方法-這主要基於下面兩個理由:
RCP采用復雜的類加載器和實例化策略來保持插件獨立性和強制實施可見性約束。我們並不想修改或替換這樣的策略來引入我們的基於容器的初始化規則。
顯式地引用這樣一個入口點(在我們例子中,是指定義在Service Locator插件中的service()方法)將強迫應用程序開發者采用一種顯式的模式和邏輯來檢索初始化的組件。這意味著,在應用程序代碼中存在某種庫鎖定。因此,我們要定義一個並不需要一種顯式的引用它的codebase的協作性插件。
由於這些原因,我們將引入在java.lang.instrument包中定義的java轉換代理(在J2SE 5.0及更高版本中才可用)。其實,一個轉換代理就是一個對象,它實現了定義一個唯一的transform()方法的java.lang.instrument.ClassFileTransformer接口。當一個轉換實例被注冊到JVM時,對於每一個在JVM中創建的類都將調用該轉換器實例。在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 Locator插件)。該代理將通過核實存在於類中的Serviceable注解來識別可服務的對象。然後,它修改所有可用的構造器-把回調添加到IoC容器-以便它能夠在實例化時刻配置和初始化該對象。
讓我們假定這個對象依賴於外部服務:
@Serviceable
public class ServiceableObject {
public ServiceableObject() {
System.out.println("Initializing...");
}
@Injected public void aServicingMethod(
Service s1,
AnotherService s2) {
// ...省略...
}
}
在代理修改完之後,它的字節碼將會與通過正常編譯這個類得到的字節碼一致:
@Serviceable
public class ServiceableObject {
public ServiceableObject() {
ServiceLocator.service(this);
System.out.println("Initializing...");
}
@Injected public void aServicingMethod(
Service s1,
AnotherService s2) {
// ...省略...
}
}
使用這種方案,現在我們能夠正確地配置可服務的對象並且使之用於應用程序中而不需要開發者"硬性地綁定"對於容器的依賴性。開發者將只需要使用可服務的注解來標志可服務的對象即可。代理代碼如下所示:
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);
//分析類文件
ConstructorVisitor cv = new ConstructorVisitor();
ClassAnnotationVisitor cav = new ClassAnnotationVisitor(cv);
creader.accept(cav, true);
if (cv.getConstructors().size() > 0) {
System.out.println("Enhancing "+className);
//生成增強的構造器類
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對象用於標識標有可服務的注解的類並且負責收集它們的構造器已經足夠了。它們的源碼如下所示:
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是一個ASM方法訪問者,這是一個允許方法操作的類
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插件,它"發行"存取服務的公共接口
這個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();
}
// ...省略...
}
這個工廠被注冊到Service locator插件作為一種Eclipse擴展,如其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.這個示例應用程序中具有一個從服務插件中檢索到的fortune cookie
六.結論
在本文中,我討論了如何利用一種強有力的編程模式-它使用一種正在出現的加速Java客戶端應用程序開發的技術(Eclipse RCP)來簡化代碼依賴性(控制反轉)處理。盡管我沒有實現其中的許多細節,但是我展示了一個示例應用程序-其中,服務與服務客戶端能夠分離開來。我還向你展示了,當同時開發客戶端和服務時Eclipse插件技術是如何保持關系分離的。然而,還有大量的有趣的內容值得探討,例如當不再使用服務時的清除策略,或者使用模擬服務對我們的客戶端插件進行單元測試的方式,等等。所有這些,只好請讀者來完成了。