本文档为 AOP Alliance 白皮书的部分翻译
引言
本文档的目的是介绍 AOP 联盟项目。它的目标,它的哲学,它应该提供什么答案,它不应该提供什么。这是一个提案草案,必须与 AOP 联盟的其他成员进一步讨论,以便就我们在这里所做的工作达成共识。一旦从列表中的讨论中出现有趣的点,它也应该去完成。
本文档是一份白皮书,可供 AOP 联盟成员用于内部目的,也可以为外部人员提供有关 AOP 联盟的见解和理解。
在第1节中,我将尝试概括地解释 AOP 联盟的目标。我们的动机来自这样一个事实,即 AOP 可以改进诸如基于 J2EE 的解决方案。如果我们设法定义一组规范化的 API,就可以将 AOP 集成到现有解决方案中或使用现有 AOP 工具构建 AOP 环境。
第2节概述了面向切面环境的拟议架构和 API。我们试着猜测 AOP 联盟应该指定哪些 API。
最后,在第3节中,我将详细介绍已识别的组件及其在 AOP 环境中的角色。
AOP 概念及术语
Cross-cutting concerns:横切关注点,指的是一些具有横越多个模块的行为,使用传统的软件开发方法不能够达到有效的模块化的一类特殊关注点。尽管 OO 模型中的大多数类将执行单个特定功能,但它们通常与其他类共享共同的次要需求。
例如,我们可能希望在线程进入或退出方法时向数据访问层中的类添加日志记录。其它的关注点可能与安全有关,例如访问控制或信息流控制。尽管每个类具有非常不同的主要功能,但执行次要功能所需的代码通常是相同的。
Aspect:切面,跨越多个类的关注点的模块化,一个切面横切了程序的核心关注点。它由切入点和通知组合而成。
例如,日志代码可以横切许多模块,但是日志的切面应该与其横切的模块功能关注点分离。将事物、日志和持久性等切面与业务逻辑隔离是面向切面编程 (AOP) 范式的核心。
Join point:连接点,程序执行过程中的一个点,例如方法的执行、字段的访问或异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法的执行,Spring 只处理访问的执行,包括构造方法。
在我们的例子中,连接点为线程进入或退出方法这一程序执行点。
Advice:通知/建议,切面在特定连接点采取的行动。许多 AOP 框架,包括 Spring,将通知建模为拦截器,并围绕连接点维护一系列拦截器。这是您要应用于现有模型的附加代码。
在我们的例子中,这是我们想要在线程进入或退出方法时应用的日志记录代码。
Pointcut:切点,匹配连接点的 predicate - 谓词。 通知与切点表达式相关联,并在切点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。由切点表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切点表达式语言。
在我们的例子中,切点为满足数据访问层类的方法这一谓词。
Introduction:引入,引入通过定义额外的接口方法或字段修改 Java 类的类型和结构,它可用于强制现有类使用另一个类的方法来实现额外的接口。它本质上允许开发人员在 Java 中创建 C++ 风格的多继承对象系统。Spring AOP 允许您向任何通知的对象引入新的接口(和相应的实现)。
在 Spring 中您可以使用
@DeclareParents
注解进行引入。此注解用于声明匹配类型具有新的父级(因此得名)。例如,给定一个名为UsageTracked
的接口和一个名为DefaultUsageTracked
的接口的实现,以下切面声明服务接口的所有实现者也实现UsageTracked
接口(例如,通过 JMX 进行统计):@Aspect public class UsageTracking { @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) public static UsageTracked mixin; @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)") public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } }
Target object:目标对象,由一个或多个切面通知的对象。也称为“通知对象”。由于 Spring AOP 是使用运行时代理实现的,所以这个对象始终是一个被代理的对象。
AOP proxy:AOP 框架为了实现切面合约(通知方法执行等)而创建的对象。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
Weaving:将切面与其他应用程序类型或对象链接以创建通知对象。这可以在编译时完成(例如,使用 AspectJ 编译器),加载时或运行时。 Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。
AOP 联盟目标
AOP 优势:J2EE 案例
J2EE 是一个典型的目标环境(但不是唯一的),可以从 AOP 联盟中受益。事实上,J2EE 环境通过提供处理技术问题(如持久性或事务)的方法部分解决了一些问题。但是,J2EE 体系结构不够灵活,无法轻松添加与特定需求相关的新技术问题。此外,当不需要或更喜欢更轻的解决方案时,能够去除一种解决方案会很有趣。
AOP 提供了一种通用方法来构建新的技术关注点(横切关注点)并以灵活和模块化的方式将它们插入到应用程序中。在 J2EE 中应用一些 AOP 概念也可以真正简化它的使用。例如,可以使用常规 Java 对象 (POJO) 代替 EJB。因此,能够轻松地将完整的 AOP 应用到 J2EE 将大大提高 J2EE 的可用性。它还将为符合 J2EE 的应用程序服务器带来更多的功能。
当前对 AOP 的约束
AOP 越来越受欢迎。然而,大多数 AOP 工具并不是为了在任何环境中应用而设计的(主要是因为大多数工具是为了实验目的而设计的)。因此,当尝试在特定环境中使用 AOP 时,我们可能会遇到一些问题,因为例如,环境已经支持一些可能不符合 AOP 工具实现的内置方面。
出现这个问题是因为 AOP 需要修改应用程序的对象/类才能正常工作。这个对象修改逻辑是由 AOP 工具的一个特定部分实现的:Weaver - 编织器。Weaver 可以很好地适应给定的环境,但可能会破坏另一个环境中的一些重要系统属性。例如,Gregor 和 Rickard 之间关于 AOP 联盟列表的有趣讨论似乎表明,AspectJ 用于引入(特定编织操作)的编织器实现并不适合某些具有某种分布式和持久性能力的环境。
AOP 联盟声明
这里的大多数人都不相信完美的系统。我们认为一个系统总是适合给定的问题和环境(它不一定适合另一个)。这正是我们可能在复杂环境(如 J2EE)中使用的 AOP 工具的情况。根据面临的问题,有一个特定的 AOP 实现会很有用。
已经有很多 AOP 或 AOP 相关技术的具体实现,例如通用代理、拦截器(interceptor)或字节码翻译器(bytecode translator)。例如:
- AspectJ:AO 源代码级别(和字节码级别)编织器。切面编程新语言。
- AspectWerkz:AO 框架(字节码级别的动态编织器 + XML 中的配置)。
- BCEL:字节码翻译器。
- JAC:AO 中间件(字节码级别的动态编织器+配置+切面)。框架。
- Javassist:具有高级 API 的字节码翻译器。
- JBoss-AOP:拦截和基于元数据的 AO 框架(在 JBoss 应用服务器上运行 + 独立版本)。
- JMangler:具有翻译组合框架的字节码翻译器。
- Nanning:AO 编织器(框架)。
- Prose:AO 字节码级别的动态编织器(框架)。
对我们来说,这些实现反映了没有好的或坏的实现,而是适合一些问题/环境的实现。
因此,AOP 联盟的目标既不是提供新的 AOP 模型,也不是提供适用于所有情况或给定 J2EE 应用程序服务器的出色 AOP 实现。AOP 联盟的目标是使所有现有的实现能够使用相同的核心语言,以便:
- 避免因为复用而重新构建现有的 AOP 组件,
- 为给定的目标环境(通常是 J2EE 环境)简化现有 AOP 组件的适配,
- 通过拥有一个通用的根 AOP API 来简化切面的复用。
- 简化希望集成 AOP 功能的开发工具的实现。
面向切面的架构
一个共同的架构愿景
在前两节中,我们解释说很难就通用 AOP 模型和实现达成一致,因为它与使用上下文和环境的联系过于紧密(实现可能在纯 Java 方法和符合 J2EE 的方法中有所不同)。但是,我们认为有可能就面向切面的环境 (AOE) 的共同架构愿景达成一致。
实际上,在构建面向方面的环境 (AOE) 时,设计师需要定义一个架构。在大多数现有的 AOE 中,架构定义并组合了一些实现系统基本功能的基本模块/组件/API。通过查看现有工具,我们可以识别通用组件(即在所考虑的架构中提供紧密功能的组件,但不一定使用相同的实现技术)。例如,JBoss 的 weaver 使用 Javassist 来实现拦截机制(在客户端植入 - Instrument),而 JAC 的 weaver 使用 BCEL 来实现拦截机制(在服务器端植入 - Instrument)。可以使用其他技术(如介入 JIT 编译器)来实现相同的效果。所有这些都严重依赖环境。
在下一节中,我们将尝试提取对 AOE 有用的组件。这些组件可用于构建上下文相关的 AOE。
三层典型架构
可以绘制一个典型的架构,如上图所示。此简化图包含一些组件(框)和一些可以使用(粗体箭头)组件 API 的核心逻辑(圆框)。它旨在图的顶部运行初始 AO 程序。请注意,此架构不打算成为参考架构,而只是一种可能的架构。实际上,组合 AO 架构的不同核心组件存在多种可能性。
可以将此架构分为三层:
- 一个低级层 (1),提供基本组件以在目标平台上实现编织(AOP 的主要过程),
- 一个高级层 (2),为 AOP 提供基本组件,在其原始含义中,加上实现 AO 语义的逻辑(将取决于目标平台),
- 开发级层 (3),包括最大意义上的 UI(可以被语言支持,可以是建模工具)以及帮助开发人员信任 AO 程序所需的其他工具(例如类型检查、可视化工具、调试器等)。
AOP 联盟在这里指定什么?
如前所述,AOP 联盟的目标不是提供新模型或现有工具的更好实现。事实上,AOP 联盟的目标是为通用面向切面环境 (AOE) 实现中标识的组件指定规范化 API。如果我们设法做到这一点,通过集成最适合我们想要使用 AOP 的上下文的组件,就有可能构建出比现有 AOE 更好的 AOE。特别是,即使在 J2EE 应用程序服务器等复杂环境中,也应该可以使用最好的 AOP。
因此,如果我们参考上图,AOP 联盟的角色应该是定义已识别组件的 API。最重要的组件是低级组件,因为它们的实现会影响可以使用 AOE 的环境。一些技术特征也可能对最终的系统属性产生深远的影响(例如,切面是否可以动态地编织/unwoven?系统是否可扩展?系统是否可以与内置切面共存,例如持久性或事务?)。但是,高级组件对于 IDE、调试器、建模工具等工具也很有趣。拥有一个通用的 AOP 概念操作 API 将有助于工具更好地支持不同环境中的多个 AOP 实现。
AOP 联盟可以为某些组件提供一些参考实现(通过使用现有工具)。但是,如果现有工具(大多数工具创建者都在联盟中)提供他们自己定义的 API 的实现,那就更好了。这些实现将验证 API 的正确性。
AOP 联盟不会解决编织逻辑和配置逻辑,因为它实际上取决于 AOE 实现。但是,我们还应该提供一些参考实现,以展示我们的 API 应该如何用于构建 AOE。
最后,AOP 联盟不会解决第三层(开发级)。我们应该让开发工具实现者在他们集成的 AOP 工具实现它时使用我们的 API。
AOP 联盟组件
现在让我们深入了解核心 AOP 联盟组件的全局图景。警告:这些组件是 AOP 联盟应指定的 API 的第一个提案草案。其中一些可能会被删除,一些可能会被添加。请注意,其中一些已经开始用 Java 接口指定。
底层组件
底层组件非常重要,因为整个 AOE 都依赖于它们来实现。这些组件的实现方式至关重要,可能会极大地影响系统的属性,例如性能、可扩展性、集成功能或安全性。
反射
反射 API 对于任何 AOE 都非常重要。事实上,编织者需要内省基础程序的类,以便应用通知(Advice)或介绍(Introduction)。例如,如果一个切入点(Pointcut)告诉一个类的所有方法都应该被通知(Advice)(使用某种正则表达式或一个 ALL 关键字),那么编织者将需要使用反射 API 来明确知道实际需要通知(Advice)的方法列表。
当编织过程在运行时完成时,SUN 的 java.lang.reflect 实现足以构建 AOE。然而,在大多数现有系统中,编织过程发生在编译时或类加载时。在这些情况下,需要反射 API 的特定实现。根据 AOP 联盟的说法,为了能够根据 AOE 的运行上下文切换底层实现,对这个 API 进行规范化非常重要。
程序植入 - Instrumentation
从编织者的角度来看,如果反射是对编织程序的读访问,则植入 - Instrumentation 是写访问。然而,在 AOP 中,允许的程序修改是一组简化的修改,该修改代表初始程序的现有结构的增量,以便切面可以正确组合在一起。由于列表中先前的讨论,这些类型的增量修改被称为 Instrumentation - 植入。
没有用于植入的标准 API。但是,与反射一样,植入可以在运行时、编译时或加载时发生。此外,对于每个类别,可以根据上下文和 AOE 的环境执行不同的实现(例如,植入可以直接在源代码或字节码上完成)。因此,对我们来说重要的是对植入 API 进行规范化,以便根据 AOE 要求更改底层实现。
拦截器框架
另一种对构建 AOE 非常有用的基础组件是拦截框架。通过动态代理,Java 提供了一个标准的 API/框架用于拦截。然而,透明度、性能等方面的一些增强可以通过其他实现来实现(其中大多数使用植入 API)。因此,定义一个具有清晰语义的标准拦截 API/框架也很有趣。
拦截框架有很多优点,因为它们允许非常容易地实现 AOP 模型的围绕通知 - Around Advice。此外,尽管它们是用纯 Java 编写的,但它们可以是独立的,并且大部分时间都提供非常清晰的类似 AOP 的代码。由于这些原因,许多项目和环境(包括 J2EE 应用服务器,请参阅 JBoss)中实现了几个拦截框架。因此,AOP 联盟应该提供一个抽象的拦截框架来规范这个 AOP 重要的工具箱。
元数据处理
元数据处理在实现 AOE 时很有用,尤其是与拦截框架结合使用时。它允许编织者以非侵入性的方式扩展类语义。由于元数据的大多数实现都允许动态性,因此它也可以用于切面的动态配置/重新配置。
即使 JDK1.5 将提供元数据的标准实现,提供一个允许多个实现的标准 API 应该是有用的。这些可能会考虑一些环境特性,例如分发、序列化,默认实现可能无法正确处理这些特性。
类加载框架
在许多 AOE 中,需要字节码级别的操作。它可以用来实现一个拦截框架,或者直接实现程序的编织植入。在某些情况下,这种字节码级别的操作可以在类的加载时完成,因为 AOP 植入非常简单。因此,大多数 AOE 使用 Java 灵活的类加载架构。
然而,一些环境也使用类加载器来实现它们自己的功能。例如,分布式环境可以使用特定的类加载器生成存根。在这些环境中,AOE 的类加载机制可能会由于类加载器不兼容而导致系统崩溃。
因此,我们认为规范化类加载框架可能很重要,该框架足够灵活,可以轻松地使来自不同环境的不同类加载器以安全的方式协作。
高层组件
如果我们希望在第三层(开发层)中定义的工具总体上为 AOP 提供更好的支持,那么高级组件对于规范化很重要。
AOP API
引用 Gregor 的话可以很好地解释我们的 AOP API 的目标:“显然,我们想尽我们所能避免 AOP 工具之间不必要的不一致。现在真正标准化还为时过早,我们仍然需要有意义的差异空间。但不必要的差异显然值得消除。”
所以,我们的 AOP 模型不会是一个新模型。它只会尝试将所有当前模型的共同点结合在一起。AspectJ 模型无疑是最成功的模型,并且已经有一些工具支持它。所以我们可能会在这里采用 AspectJ 的一个子集。
配置 API
许多切面可以以通用方式实现。这意味着它们实现了一种逻辑,对于您希望在其中编织切面功能的任何程序,该逻辑都可能可重用。大多数情况下,这个切面重用过程意味着通用方面的参数化(例如,告诉一个通用持久性切面哪个类应该持久化以及如何持久化)。在 AspectJ 中,这可以通过对抽象切面进行子类化来完成。但也可以通过使用外部工具(例如预处理器)来完成。在 J2EE 环境中,内置切面(EJB 容器的技术关注点)的配置过程由 XML 部署文件参数化。在 JBoss/AOP 等框架中,也可以使用 XML 文件进行配置。在 JAC 中,可以使用切面配置 API 在 Java 程序中完成配置,也可以使用特定的类似脚本的语言,等等。
如果我们可以规范化配置 API,那就太好了。这将使开发工具的 AOE 集成更容易。它还有助于将特定配置从 AOE 重用到另一个 AOE(例如 AspectJ 和 JBoss/AOP)。
请注意,由于潜在的重要差异,将切面从一个 AOE 移植到另一个可能是不现实的。但是使切面配置可移植似乎不那么不切实际,这已经是实现 AOE 互操作性的第一步。
结论
本文试图解释 AOP 联盟项目的原因,并指定一些目标。我试图以名单上许多人的名义就我所了解的内容发言。目前它是相当不精确的,并且只绘制了一个全局图。也许有些人会不同意或会失望。如果我们能进行一些非常好的讨论,帮助我们就我们真正想要的东西达成良好的感觉,那就太好了。