JRockit JVM对AOP的支持,第2部分
前一篇文章
介绍了面向方面编程和
关注点分离的概念,解释了这种概念如何在方面构造的帮助下增强软件的模块化,以及如何使用它来补充面向对象编程。方面代表模块化的单元,并且由切点(何处)、建议(什么)以及类型间声明(在这个新的方面补充对象模型)组成。有许多技术可以将关注点
编织进应用程序,在当今的Java领域中,最常用的技术是字节码测试,在
AspectWerkz和
AspectJ(从1.1版开始)中实现了这种技术。
但是,这种AOP实现方式具有几个缺点,本系列的第1篇文章对此进行了详细解释。尽管在字节码测试领域还有很大的发展余地(包括Java 5中的JVMTI/JSR-163测试代理规范和高效字节码操作库,比如ObjectWeb ASM),但字节码测试代价不菲。此外,已经证明,使用字节码测试实现AOP是不完善的。例如,如果不采用非常特殊且效率低下的解决方案,就无法通过切点匹配反射式方法调用或get和set字段。总的来说,所有基于字节码测试的产品都受到字节码测试技术相关问题的影响,而且随着这种技术的普及,问题将逐渐增加。
所有这些缺点促使JRockit团队提出了JVM对AOP的支持。其目标是尽可能全面地实现当前的AOP语义,同时不把JVM限制在某个特定的面向方面框架的语言细节和编程模型上。
本文通过具体的代码示例介绍该API,然后描述其好处及未来的发展方向。
我们的动机 让我们快速地回顾引入JVM的AOP支持的技术动机。
JVM编织是对上面提到的问题最自然的解决方案。为了说明其原因,我们将引入两个例子,它们说明JVM已经完成了编织所涉及的大多数工作:当加载一个类时,JVM读取字节码,建立为java.lang.reflect.* API进行服务所需的数据;另一个例子是方法调度。目前的JVM将方法或代码块的字节码编译为更高级、效率也更高的构造和执行流(在适用代码内联的地方进行代码内联)。由于HotSwap API的需要,JRockit JVM(可能还包括其他JVM)还会记录哪个方法调用了其他方法,因此如果在运行时重新定义某个类,那么在所有期望的位置(内联的或非内联的),类中定义的方法主体仍然可以进行热交换。
因此,不必为了编织进一个建议调用而修改字节码,比如说,在特定的方法调用之前。JVM实际上可以掌握关于这个建议调用的知识,它会在任何匹配的联结点上对此建议进行调度,然后再调度实际的方法。
由于不接触字节码,立即可以获得以下好处:
- 不会由于字节码测试而产生启动开销。
- 对于在任何位置、任何时间、以递增式开销添加和删除建议的完全的运行时支持。
- 对建议的反射式调用的隐式支持。
- 不需要将类模型复制到特定于框架的某些结构,因此减少了额外的内存占用。
与JVMDI_EVENT_METHOD_ENTRY或JVMDI_EVENT_FIELD_ACCESS等JVMDI规范中定义的众所周知的C级别事件相比,这种方式有很大区别。在JVMDI中,必须首先处理C级别API,这使得它对于大多数开发人员来说有些复杂,而且难以分发。其次,规范没有提供细粒度的联结点匹配机制,而是要求预定所有这样的事件。这仍然会导致显著的开销,因此不得不进行调试。
我们的方法 我们想让您先了解一下如何在JVM中添加AOP支持。关键之处在于我们在Java API级别上提供了动作调度和预定(下面会详细描述)。因此,您可以写出下面这样的代码:
Weaver w = WeaverFactory.getWeaver();
Method staticActionMethod =SimpleAction.class.getDeclaredMethod
("simpleStaticAction",new Class[0]//no arguments);
MethodSubscription ms = new MethodSubscription
(/* where to match*/,InsertionType.BEFORE,staticActionMethod);
w.addSubscription(ms);
如您所见,我们提供了一个可访问的JVM API,可以用它来实现更传统的AOP方法。这为解决前面提到的传统AOP实现问题提供了极大的灵活性,而且也使其他使用方式成为可能。下面几节将详细介绍这个API。
动作调度和预定 JRockit JVM AOP支持公开了一个Java API,它与JVM方法调度和对象模型组件紧密集成在一起。为了确保不使JVM被限制在当前或未来的任何特定于AOP的技术方向上,我们决定实现一个动作调度和预定模型。
这个API使您能够在指定的切点上描述定义良好的预定,这样就能够注册JVM将要调度的动作。动作由以下组件组成:
- 一个常规Java方法——我们称之为动作方法,对于每个匹配这个预定的联结点,都将调用这个方法。
- 一个可选的动作实例,在这个实例上调用动作方法。
- 一组可选的参数级注释,它们向JVM指出动作方法期望从调用堆栈获得哪些参数。
动作还可以分为before动作、after returning动作、after throwing动作或者instead-of动作(类似于AOP的“around”概念)。
为了调用这个API,必须获得一个jrockit.ext.weaving.Weaver实例的句柄。这个编织器实例根据它的调用者上下文来控制允许进行哪些操作。例如,在容器级编织器可以预定特定于应用程序的联结点时,用户可能不希望部署在应用服务器中的应用程序创建编织器,从而预定某些容器级或特定于JDK的联结点的动作方法。这种编织器可见性理念反映了底层类加载器的委托模型。
我们简单介绍一下这些构造如何映射到常规的AOP构造,这有助于理解这个模型:
·预定可以视为一个有类型的联结点,或者就是一个有类型的联结点(字段get()、set()、方法call()等等),加上一个within()/withincode()切点。
·动作实例可以视为方面实例。
·动作方法可以视为建议。
熟悉AOP的读者可能已经看出,要想用这个JVM级API实现一个完整的AOP框架,还需要进行一些开发,包括一个(按照规定)管理方面实例化模型的中间层、cflow()切点的实现以及切点的完全合成和正交的实现。
API细节:动作方法 动作方法(与AOP的建议概念相似)就像(作为方面的)常规类的常规Java方法。它可以是static方法,也可以是成员方法。它的返回类型必须符合某些隐式约定,而且before动作的返回类型应该是void。对于instead-of动作(类似于AOP的around建议语义),其返回类型还是作为动作调用结果的堆栈的类型。
动作方法可以有参数,参数的注释进一步控制上下文公开,如下面的代码示例所示:
import java.lang.reflect.*;
import jrockit.ext.weaving.*;
public class SimpleAction
{ public static void simpleStaticAction()
{out.println("hello static action!");
}
public void simpleAction() {out.println("hello action!");
}
public void simpleAction(@CalleeMethod WMethod calleeM,@CallerMethod WMethod callerM)
{out.println(callerM.getMethod().getName());
out.println(" calling ");
out.println(calleeM.getMethod().getName());
}
}
该代码示例引入了jrockit.ext.weaving.WMethod类。该类用作java.lang.reflect.Method、java.lang.reflect.Constructor和类的静态初始化器(它在java.lang.reflect.*中没有出现)的包装器。这与AspectJ JoinPoint.StaticPart.getSignature()抽象化相似。
下面是当前定义的注释及其含义。
注释 | 公开 | 备注 |
@CalleeMethod | 被调用者方法(方法、构造函数、静态初始化器) | |
@CallerMethod | 调用者方法(方法、构造函数、静态初始化器) | |
@Callee | 被调用者实例 | 滤除静态成员调用。用作instance-of型过滤器:被调用者类型必须是所注释的参数类型的实例。 |
@Caller | 调用者实例 | 滤除来自静态成员的调用。用作instance-of型过滤器:调用者类型必须是所注释的参数类型的实例。 |
@Arguments | 调用参数 | |
为了支持instead-of,并能够决定是否沿着截取链前进(就像在AOP中通过JoinPoint.proceed()概念实现),我们引入了jrockit.ext.weaving.InvocationContext构造,如下所示:
import jrockit.ext.weaving.*;
public class InsteadOfAction {
public Object instead(
InvocationContext jp,
@CalleeMethod Method callee) {
return jp.proceed();
}
}
API的细节:动作实例和动作类型
正如前面代码示例中所示,动作方法可以是静态的,也可以不是。如果动作方法不是静态的,那么就必须传递一个动作实例,JVM在这个实例上调用动作方法。
其语法风格与Java开发人员使用java.lang.reflect.Method.invoke(null/*static method*/, .../*args*/)对方法进行反射式调用一样。但是,利用JVM的AOP支持,底层的动作调用根本不涉及任何反射。
允许用户控制动作实例,就会产生有趣的用例。例如,可以实现一个简单的委托模式,在运行时用另一个实现替换整个动作实例,而不涉及JVM的内部组件。
注意,这将有助于(按照规定)实现AOP方面实例化模型,比如issingleton()、pertarget()、perthis()、percflow()等等,同时不会将JVM API限制在某些预定义的语义上。
在将预定注册到编织器实例之前,赋予它一个类型作为建议类型:before、instead-of、after-returning或after-throwing。
可以编写下面这样的代码来创建预定:
// Get a Weaver instance that will act as a
// container for the subscription(s) we create
Weaver w = WeaverFactory.getWeaver();
// regular java.lang.reflect is used to refer
// to the action method "simpleStaticAction()"
Method staticActionMethod =
SimpleAction.class.getDeclaredMethod(
"simpleStaticAction",
new Class[0]//no arguments
);
MethodSubscription ms = new MethodSubscription(
.../* where to match*/,
InsertionType.BEFORE,
staticActionMethod
);
w.addSubscription(ms);
该代码示例假设用户使用静态动作方法实现。也可以使用实例方法编写这个示例,在这种情况下,应该传递给MethodSubscription一个包含类实例。
// Use of an action instance to refer to the
// non static action method "simpleAction()"
Method actionMethod =
SimpleAction.class.getDeclaredMethod(
"simpleAction",
new Class[0]// no arguments
);
// Instantiate the action instance
SimpleAction actionInstance = new SimpleAction();
MethodSubscription ms2 = new MethodSubscription(
...,// where to match, explained below
InsertionType.BEFORE,
actionMethod,
actionInstance
);
w.addSubscription(ms2);
诸如within()和withincode()类型模式之类的AOP语义也通过该API的变体实现。
API细节:预定
如前面的代码示例所示,预定API依赖于java.lang.reflect.*对象模型和一些简单的抽象化(比如jrockit.ext.weaving.WMethod)来合并方法、构造函数和类的静态初始化器处理。
new MethodSubscription(...)调用的第一个参数必须是jrockit.ext.weaving.Filter实例,这个实例具有几个具体实现以便匹配方法、字段等等。
jrockit.ext.weaving.MethodFilter实例用作定义,JVM编织器实现根据它进行联结点阴影匹配(shadow matching)。jrockit.ext.weaving.MethodFilter允许根据以下各项进行过滤(还提供额外的结构支持within()/withincode()语义):
- 方法修饰符(比如使用java.lang.reflect.Modifier时的int)。
- Class<? extends java.lang.annotation.Annotation>,匹配方法运行时可见性注释。
- jrockit.ext.weaving.ClassFilter实例,匹配声明类型。
- jrockit.ext.weaving.StringFilter实例,匹配方法名。
- jrockit.ext.weaving.ClassFilter实例,匹配方法返回类型。
- jrockit.ext.weaving.UserDefinedFilter实例,用于实现更精细的匹配逻辑。
使用jrockit.ext.weaving.UserDefinedFilter回调机制来实现更高级的匹配方式(与Spring AOPorg.springframework.aop.MethodMatcher和org.springframework.aop.ClassFilter相似)。
所有这些结构都是可选的,如果遇到null,就表示“任意匹配”。
jrockit.ext.weaving.ClassFilter提供一种类似的方式:
- Class<? extends java.lang.annotation.Annotation>,匹配类运行时可见性注释。
- Class,匹配类类型。
- boolean值,表示是否匹配子类型。
因此,以下代码匹配所有名称以“bar”开头的方法调用。注意,在这个非常简单的例子中传递了好几个null值:
StringFilter sf =
new StringFilter("bar", STARTSWITH);
MethodFilter mf =
new MethodFilter(0, null, null, sf, null, null);
MethodSubscription ms = new MethodSubscription(
mf,
InsertionType.BEFORE,
staticActionMethod
);
w.addSubscription(ms);
作为更现实的例子,以下代码匹配所有三个EJB业务方法:
// Prepare the pointcut to match
// @Stateless annotated classes business methods
MethodFilter ejbBizMethods = new MethodFilter(
PUBLIC_NONSTATIC,
// Method annotation does not matter
null,
new ClassFilter(
// Declaring class, the java.lang.Class
// for the EJB we are currently manipulating
ejbClass,
// no subtypes matching
false,
// class annotation
Stateless.class
),
// EJB methods matching is handled
// in a UserDefinedFilter below instead
null,
// return type does not matter
null,
// custom Filter callback
new UserDefinedFilter() {
public boolean match(
MethodFilter methodFilter,
WMember member,
WMethod within) {
return !isEjbLifeCycleMethod(member);
}
}
);
好处
使用JVM编织而不是字节码测试有几个好处。从较高的层面来看,编织作为JVM功能的自然扩展出现,因此在许多方面它不那么具有侵入性,并且为性能、可伸缩性和可用性各方面带来了许多好处。
关于字节码编织的问题(尤其是在加载时编织的情况下)的详细讨论,请参考本系列的第1部分。以下好处解决了所有这些问题。
字节码没有被修改。在JVM内部组件中,仍然采用从字节码到可执行代码的常规编译管道。使用字节码测试时,需要分析字节码指令并用某些中间结构来表示它们,这样才能在测试框架(AOP编织器或基于字节码测试的产品)中操纵它们;而使用JVM编织不需要这么做。
编织器变得无所不在了。即使用户希望在启动时注册预定,这也不再是必须的。因为根本不需要分析字节码指令来寻找要截取的联结点,所以大大减少了应用程序的启动时间。这也提供了开发真正动态的系统的机会——动态意味着可以在任何时候部署方面和解除方面部署,而又不会由此引起额外的开销或复杂性。
- 不使用冗余的类型信息记录,降低了内存耗用并且提高了可伸缩性
因为不再进行字节码测试,因此与对象模型双重记录问题相关的问题就不会出现了。预定API依赖于java.lang.reflect.*模型,而这个模型已经以类似的方式向Java开发人员提供了此信息。
因为所编织的类的字节码没有经过修改,所以不会因为两个不同的代理以不兼容的方式修改字节码(相互隐藏原始程序的属性),而造成冲突。预定的注册次序起到了优先权规则的作用。注意,如果类是可序列化的,那么不会为了在运行时执行所编织的建议而向其添加隐藏结构,所以常规的序列化将得到充分支持。而字节码测试技术通常需要确定序列化能力是否有所保留(例如,serialVersionUID字段的处理)。
通过使用JVM级方法调度,所有反射式调用(方法调用或者get或set字段)都可以被匹配,就像它们是常规调用,而且所有注册的动作都将被触发一样。这不需要任何额外的开销,也不涉及特定于实现的细节和复杂性。
未来的发展方向
尽管JVM编织很有帮助,而且解决了与字节码测试技术相关的可伸缩性和可用性问题,但是仍然必须解决一些缺陷才能使其完美地实现用例,这可能需要采用一些补充方法。
一些基于字节码测试的产品使用了细粒度更改,当前的JVM AOP API还无法实现这一特性。某些用例处理同步块,因此不同的锁定机制(如:分布式锁定)可以透明地注入常规的应用程序。这样的细粒度动作常常要求对同步块进行有条件执行,甚至完全删除同步块,并使用某个专用锁定API调用来替换它。可以在JVM中解决这样的特定需求,但是实际上不可能找到一个对每种用例都有效的高效解决方案。还有必要提醒一下的是,目前领先的AOP框架还不能将同步块公开为联结点。
在JVM级别上,无法轻松地实现AspectJ定义的某些细粒度语义。例如,AspectJ支持预初始化、初始化和构造函数执行切点。构造函数执行切点挑选出源代码中出现的构造函数,初始化切点挑选出获得已初始化实例的所有构造函数执行,包括this(...)构造函数委托。JVM难以把握这两者的差异。更具侵入性的代码内联策略可能会出现在哪些地方实际上也可能取决于编译器。
随着字节码测试逐渐流行起来,新的JVM API的引入肯定会遇到挑战。如果要开发一种同时适应两种JVM(支持新API的JVM,比如JRockit,以及不支持新API的JVM)的产品,那么成本会相当高。这个领域中的规范(如:JSR)可能有助于克服这种困难。
结束语
字节码测试技术目前已经在不同领域的Java平台上得到广泛使用,从面向方面软件开发到更特定于应用的解决方案(如:应用程序监控、持久性或分布式计算)。随着字节码测试的可用性和透明性的提高,加载时编织和部署时测试将会流行起来。
遗憾的是,这种技术没有为可伸缩性和可用性需求提供适当的支持。特别是随着这种技术的应用越来越广泛,以及对来自不同产品的不同测试代理的混合使用,这个问题会越来越严重。JVM编织和JVM对AOP的支持(比如在JRockit中所实现的)是解决这个问题的自然方法,可以促进革新和技术发展。JRockit团队所提出的Java API将JVM方法调度内部组件与用户定义的动作联系起来,仅依赖于java.lang.reflect API的预定优雅地填补了以前的鸿沟,并解决了主要的可伸缩性和可用性问题。
这种新的API要想获得广泛采用,需要对它进行认真的评估,并将它应用于真实的用例,比如AOP或者大型应用程序的运行时自适应。
posted on 2006-04-01 11:20
aa611 阅读(104)
评论(0) 编辑 收藏