当前页面: 开发资料首页 → JSP 专题 → 在Eclipse RCP中实现反转控制(IoC)
摘要: 在Eclipse RCP中实现反转控制(IoC)
@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服务对客户端插件进行单元测试,这些问题我将留给读者去思考。