@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插件时采用一种透明的方式,原因有二:
出于这些原因,我将引入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应用程序示例
现在我们具有了构建应用程序的所有元素。我们的例子可用于显示用户感兴趣的名言警句。它由四个插件组成:
采用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服务对客户端插件进行单元测试,这些问题我将留给读者去思考。
新闻热点
疑难解答