扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
作者:中国IT实验室 来源:中国IT实验室 2007年9月24日
关键字:
在本页阅读全文(共2页)
ASM and java.lang.instrument Agents
以上介绍的各种注射策略都依赖于容器的存在,依赖容器提供一个入口点来使得应用程序可以使用该入口点来请求已经得到正确配置的对象。但是,我们想在开发我们的IoC插件的时候使用一种透明的方式,这主要有两个原因:+RCP采用了复杂的classloader以及初始化策略(考虑一下createExecutableExtension())来维护插件的隔离性以及可见性的限制。我们不想修改或者替代这种策略来引入我们的基于容器的初始化规则。
+对这样一个入口点(这个例子中,是service locator插件中定义的service()方法)的显式引用将会强迫应用程序开发人员采用一种显式的模式和逻辑来获取初始化的组件。这样就会有一些应用-锁定的类库出现在程序代码中。我们想要定义一个合作的插件,这个插件并不需要显示的引用这些代码。
出于以上原因,我们要引入定义在java.lang.instrument包中的Java转换代理,这个包出现在J2SE5.0以及以后的版本中。一个转换代理是一个实现了java.lang.instrument.ClassFileTransformer接口的对象,这个接口只有唯一一个方法,transform()。当一个转换器的实例注册到JVM中后,当JVM要创建任何类的时候,这个代理的transform()会被调用。转换器可以在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 vlocator插件)。这个代理会通过检验是否存在Serviceable标注来确定一个可服务对象。然后,修改所有的构造函数来调用IoC容器的方法在对象初始化时正确地配置和初始化这个对象。
让我们假设我们有一个需要依赖于其他服务的对象(还记得Injected标注吗?):@Serviceablepublic class ServiceableObject { public ServiceableObject() { System.out.println("Initializing……"); } @Injected public void aServicingMethod( Service s1, AnotherService s2) { // …… omissis …… }}
当它被转换代理操作以后,它的字节码会和下面这个进行正常编译的类的字节码一样:@Serviceablepublic 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使用访问者模式来将类数据(包括指令序列)作为事件流处理。当解码一个已经存在的类时,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 manipulationmv.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插件,发布访问服务需要的公共接口。
+客户端插件,作为Eclipse应用在一个Eclipse视图中显示格式化的警句。
我们采用的IoC设计使得服务的实现和客户端分离,服务的实现可以改变或者修改,而客户端却不受影响。
就像上面几节描述的那样,为了向用户显示警句,service locator将会将客户和服务绑定在一起。FortuneInterface仅仅定义一个公有的接口,用户可以使用它来访问cookie消息。
public interface IFortuneCookie { public String getMessage();}
Fortune插件提供了一个简单的服务工厂来创建IFortuneCookie的实现类的实例。
public class FortuneServiceFactory implements IServiceFactory { public Object getServiceInstance() throws ServiceException { return new FortuneCookieImpl(); } // …… omissis ……}
工厂作为一个Eclipse扩展注册到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视图使用。
@Serviceablepublic 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对象而且可以保证cookie是正确初始化过的。
结论
在这篇文章中,我们讨论了如何将一种强大的设计模式(可以简化代码依赖的处理,IoC)与一种可以加速Java客户端应用程序开发的技术合并在一起。虽然我没有处理更多于这个问题有关的细节,但是我也已经演示了一个示例应用如何将服务和服务客户解耦。我同时也描述了Eclipse插件技术在开发客户端和服务时如何做到了关注点分离。但是,仍然有很多有趣的元素在等待我们探索,例如清除策略,用来在不需要这个服务的时候清除掉服务,或者在我们的客户插件中使用mock-up服务来进行单元测试,这些要留给读者去研究了。
Resources +本文的示例代码+Eclipse.org网站上的Eclipse RCP教程+ASM网站上的ASM 2.0介绍+Matrix,与Java共舞+http://www.onjava.com
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者