当前页面: 开发资料首页 → 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服务对客户端插件进行单元测试,这些问题我将留给读者去思考。