AOP@Work:使用AspectJ5检验库方面

编写符合所有类型用户需求的可重用方面

简介:AspectJ 5 新的语言和部署特性简化了库方面(library aspect), 而 库方面又保证一般的开发人员能够掌握 AOP。尽管有着不可思议的易用性,但它 们编写起来非常困难。在 AOP@Work 系列 的这部分内容中,Wes Isberg 编了一个假想的故事,故事所讲述的世界离您的现实生活并不遥远,其 中 有 30 个重大的挑战。通过这个故事,您将学会如何使用及编写库方面,以及如 何为相信这一技术和不相信这一技术的人交付解决方案。

救命!

一名遇险的少女上气不接下气地跑到您面前:“救命! 测 试时一切都好好的,但部署之后,系统突然停止了。没有异常!什么都没有!没 有人知道该怎么做!世界和平正濒临危险!”

您没有多问一个字, 只是打开背包,取出两个瓶子,“试试这个:”

java -javaagent:aspectjweaver.jar -classpath "vmErrorAspect.jar:${CLASSPATH}" ..

一分钟后,弹 出一个堆栈跟踪。某人在某处将所有 Error 记入了日志,包括 OutOfMemoryError ,并继续运行。多亏了您的方面 RethrowVMError !

从魔法瓶到具体方面

实际上,上面提到的两个瓶子中并没有任何魔法: 仅 仅是库方面中的通知(advice),即用装入时织入部署的。库方面 RethrowVMError 会在任何急切的错误处理程序抛出错误之前运行通知,防止其 隐 藏 VMError。AspectJ 5 拥有一些可简化库方面编写的新的语言特性,以及一些 简化具体方面使用的新的部署选项。这些特性和选项共同使经验较少(在使用容 错构建或部署流程方面经验较少)的广大新用户容易掌握 AOP —— 但前提是库方面必须得到很好的调整。作为一名编写库方面的专家 —— 至少是为读者介绍足够知识的作者,我借本文诚挚邀请各位亲 爱 的读者积极提出恰当的问题,并根据相应的回答部署简单的库方面解决方案。

RethrowVMError 是一个简单但强大的解决方案,易于理解和使用。其他 库方面的能力、可理解性和工具与之相比要更为综合。什么才是出色的库方面? 我认为,成功交付库方面源于按用户的需求和技能为其量身订做解决方案,而不 是试图提供最强大或最具可重用性的解决方案。在本文中,您将从一个简单的小 故事中了解到如何从评估用户着手设计方面。作为故事的主角,您的伟大任务就 是为终端用户编写库方面,这些用户的水平参差不齐,从简单的 XML 部署人员 一 直到 Java™ 和 AspectJ 程序员。在故事的发展过程中,您还要对一些希 望成为 AOP 专家的人进行培训,并说服一名持不信任态度的主管采纳 AOP。在 取 得最后的成功时,您将看到,树立所有人对 AOP 信心的关键正是坚实地走完每 一 步。

AspectJ 5 中的可重用方面

AspectJ 5 使可重用方面的编写 比以往任何时候都要更加容易。首先,它有着扩展方面和编写切入点(pointcut )的新形式,不仅在纯粹的 Java 方支持 Java 5 语言特性,更在 AspectJ 语 言 内部支持这些特性。Java 5 注释使那些从未接触过方面的 Java 开发人员能够 理 解甚至编写方面。同时,泛型类型为类型的限定增加了安全性和新方法,尤其是 对于新参数化库方面的用户。本文中介绍的方面示范了所有这些特性。

其 次,AspectJ 5 使得编写和部署方面更为容易,且不需使用 AspectJ 编译器。 在 编写方面,它支持将方面作为纯 Java 代码的注释编写,如 AspectWerkz。这些 注释风格 的方面可由 javac 编译,并随后使用织入器织入。在部署方面,新的 装入时织入器支持 XML 配置文件 META-INF/aop.xml,允许您为一个具体方面中 的抽象库方面声明具体切入点。第一批面世的库方面很可能会采用这种形式,因 为用户不需要了解关于 AspectJ 的一切内容,只需完成部署库所必需的最低限 度 XML 编辑工作。

有了这些部署 AspectJ 库的新方法之后,用户就只需要 知道部署其方面所必需的内容。装入时织入器最小化了开发和构建流程的影响, 方面库可最小化所需的专门技术 —— 但前提是库开发人员能够应对 挑战,编写出健壮的方面,并使其与部署人员提供的最小化规范协同工作。

让我们开始游戏吧!

您做好迎接库方面挑战的准备了吗?最好穿 好您的盔甲,因为您的公主 Dee 正在危急之中,她与其他许多漂亮的女士一起 工 作,她们正在等待您的帮助。像 Erin 和 Faye 这样的新手主要是想获得一种解 决方案。Gail 和 Holly 这样的中级开发人员除了想获得解决方案外,还想了解 细节。而 Irene、Jodi、Kelli、Liz 和 Mary 这些专家都希望构建自己的解决 方 案。所有这些人现在都遇到了难题。快去帮助这些女士吧!

Arnold、 Buddy 和 Connor 也与 Dee 一起工作,但他们的目标是能够帮助其他人解决问 题 ,而不仅仅是解决自己所面对的问题。在看到您对 RethrowVMError 的高效处理 后,他们也渴望成为英雄。他们能够很快地做出总结,并为自己负责的领域设定 防御措施。Arnold 对切入点带来的变化大吃一惊,Buddy 几乎不能相信注释和 mixin 接口的强大能力(但更希望获得更好的代码),Connor 打算合并库方面 。 女士们期待您为她们找到解决方案,同时这几位小伙子也迫切希望通过您的教授 而精通方面。小伙子们,不要太心急!

Zed 是主管,拥有最终决定权。Zed 憎恶更改开发流程或在某些需要大量经 验 的东西上投资,但如果时机合适,他也不会怯于应允。为了解开发人员能以多快 的速度学会编写库方面,Zed 派那几位小伙子追随在您身边,偶尔帮您做一点事 情。如果解决方案能够令女士们满意,而且小伙子们学会了编写方面,基本上 Zed 会非常乐意接受方面库。对您的判断将取决于您是否能够同时完成解决方案 和培训这两项任务。

您的口袋中有大概 30 个方面 —— 如果愿意,您可以 立即拿到它们!在本 文结尾处的 “库方面一览” 中可以看到其摘要。

Erin 利用错误声明审查代码

Erin 负责代码审查,所以 Dee 将无法处理 VMError 的有关情况汇报给了 Erin。由于存在因未曾使用 VMError 这个词而无法确定 VMError 的可能性,除 了方面以外,Erin 找不到什么好办法来完成这个任务。经过 Zed 的首肯,Erin 私下与您探讨了相关情况,告诉了您她想检查的内容,但不希望仅通过自己的眼 睛费力地检查代码。评估了她的需求和技能后(与处于她这个职位的许多人一样 ,她实际上并不能自行编写代码),您向她说明了编写基本 “within” 切入点 以指定受影响类型的方法,还给了她一组方面,共 4 个。在某些规则被违背时 , 表 1 中的每个方面都会在织入时提示存在错误。

表 1. 用于错误检查的方面

InstanceFieldNaming 禁止实例字段名不以 “f” 开头 NoCallsIntoTestFromOutside 禁止从产品包到测试包的引用 UtilityClassEnforced 禁止构造实用工具类 NoSystemOut 禁止使用 System.err 或 System.out GetterSetter 禁止在除初始化或 getter 方法以外进行字段读取操作,禁止在初始 化或 setter 方法以外进行字段写操作

为部署这些方面,Erin 编写了一些具体方面,均与清单 1 所示代码段类似 。 您可以很快地教会她指定所需类型,也就是 com.magickingdom 包之内或之下的 类型:

清单 1. withinTypes

aspect CompanyGS extends GetterSetter { protected pointcut withinTypes(): within(com.magickingdom..*);}

Erin 还可在 AspectJ 5 中通过 aop.xml 实现相同的目的,如清单 2 所示 :

清单 2. 在 aop.xml 内声明具体方面

  

若向命令行发出错误,则错误的形式与普通编译器错误类似,除非有返回到 定 义该错误的声明语句处的引用,如清单 3 所示:

清单 3. 错误声明消息

C:/article/testsrc/com/isberg/articles/aop7 /invariants/GetterSetterDemo.java:28 [error] non-public field-set outside construcTor or setter methodi++; ^^^^field-set(int com.isberg.articles.aop7.invariants.GetterSetterDemo$C.i)see also: C:/article/src/com/isberg/articles/aop7 /invariants/GetterSetter.aj:24

AspectJ Development Tools for Eclipse (AJDT) 进一步简化了 Erin 的工 作。错误和警告将随同其他编译器错误和警告一并列出,如图 1 所示:

图 1. 所声明的错误与编译器错误一并列出

在出问题的代码处,最左侧的页面空白处将显示一个带有上下文菜单导航项 的 标记,从而使 Erin 可以跳转回错误声明处,如图 2 所示:

图 2. 从代码返回到错误声明处的引用

Arnold 学会使用 GSetter 方面

Erin 的方面查找出了 Arnold 代码中的一些违规错误。通过 AspectJ 的向 后 和向前链接,Erin 可以选择更正代码或调整错误声明。根据检查,错误绝大多 数 都是违规字段,这些字段不应通过 setter 方法访问。由于 Arnold 非常喜爱切 入点,因此您教了他如何忽略违规字段,如清单 4 所示:

清单 4. 忽略违规字段

aspect CompanyGS extends GetterSetter { protected pointcut withinTypes(): within(com.magickingdom..*)  && !get (volatile * *) && !set(volatile * *);}

Arnold 和 Erin 都非常高兴,但 Zed 指出 Arnold 必须理解改变局面的底 层 切入点。这可行吗?查看了其他方面后,Zed 问道:“谁理解 NoCallsIntoTestFromOutside 中的这个切入点?” 如清单 5 所示:

清单 5. 避免到测试包的引用

pointcut referToTestPackage():  call(* *..test..*.*(..)) || call(*..test..*.new(..))  || get(* *..test..*.*) || set(* *..test..*.*)  || get(*..test..* *) || set(*..test..* *)   || (staticinitialization(!*..test..*)    && staticinitialization(*..test..*+))  || call(* *(*..test..*, ..))   || call(* *(*, *..test..*, ..))  || call(* *(*, *, *..test..*))  || call(* *(.., *..test..*))  || execution(* *(*..test..*, ..))  || execution(* *(*, *..test..*, ..))   || execution(* *(*, *, *..test..*, ..))  || execution(* *(.., *..test..*))  ;

Arnold 试图解释,但 Erin 只是翻了一个白眼。问题在于:对库方面部署者 仅需了解很少相关内容这样的假设存在风险。Zed 询问是否能够通过错误声明找 出其中的方法未得到实现的类。您不得不承认 AspectJ 仅能检查各联结点 shadow 是否有效。它无法找出不存在的联结点 shadow,也无法对程序结构作出 一般性断言。针对这些任务,您推荐使用 JQuery,而 JDepends 则适于进行相 关 性检查。得到这些说明后,Zed 表示,由于方面是可选的(对于编译程序而言非 必需),因此可以在开发时使用。但就重要的静态检查而言,Erin 或许应该使 用 一些更有意义的方法。

Faye 的失败促进了样本代码的消除

Faye 在代码审查时未能遵照 Erin 的命令完成工作,由于此后还有许多静态 检查工作,所以她希望您能提供一些帮助。她负责处理包含大量样本代码的最优 方法。经过简短的探讨后,您给了她 3 个抽象方面:

表 2. 样本代码的方面

EqualsBoilerplate 在出现匹配之前一致地处理所有为空的情况 (Object) NoNullParameters 在公共方法传递空参数时抛出异常 TrimInputStreamRead 将任何 read(..) 调用调整到可用字节

Faye 像 Erin 那样部署了具体子方面,两个人都非常满意。但 Zed 担心样 本 方面将要部署在产品代码中(即便样本方面是可选的,且程序可在不使用它们的 情况下进行编译)。Buddy 参与了谈话,他认为 EqualsBoilerplate 是一种聊 胜 于无的解决方案。它在调用之前 检查调用目标是否为空,从而避免了若干 NullPointerException。由于必须在每一个调用 equals(..) 的地方进行这样的 检查,所以 Zed 认为在这些方面未出现问题之前,可以暂时使用。

Gail 收集异常日志

Gail 要做大量记录,这使她忙得不可开交,甚至于无法参与讨论。Zed 要求 她提出记录异常日志的解决方案。她对代码块相当熟悉,希望能找到其他方法。 了解她的需求后,您为她提供了一些用于记录日志的简单方面,如表 3 所示:

表 3. 简单的日志记录方面

SystemStreamsToLog 将系统流调用重定向到日志记录程序 ObserveThrown 除非忽略,否则将抛出的所有异常记录到日志中 ObserveThrownContext 与 ObserveThrown 类似,不同之处是带有联结点上下文

Gail 可使用切入点(这将令 Arnold 分外高兴)和普通的 Java 方法重写( 这是 Zed 赞成的方法)来调整方面。与配置时仅需简单切入点的库不同, ObserveThrown 具有重写方法 observeException(Throwable)、getLogLevel (Throwable) 和 ignoreException(Throwable)。默认情况下,方面会使用自己 的 与切入点相关的日志记录程序,这对 Gail 来说比每个类的日志记录程序更有意 义。Gail 可部分地理解库方面,因为它通常委托给超类 ThrownObserver 中的 方 法,如清单 6 所示:

清单 6. 观察异常

public abstract aspect ObserveThrown extends ThrownObserver {  abstract protected pointcut observe();  /** Observe exception */ after() throwing (Throwable thrown) : observe() {  // skip if ignored or registered  if (observingThrown(thrown)) {   // log or ??    observeException(thrown);   // register to avoid duplicate calls   registerThrown(thrown);  } }}

观察了新日志后,Zed 对其一致性非常满意,但就仅记录堆栈跟踪的情况提 出 了反对意见。您推荐了另外一种库方面 ObserveThrownContext,使其可重写 getPerJoinPointMessage(Throwable, JoinPoint)。这在 ObserveThrown 中并 非 默认,原因是对联结点的反射访问既耗时间、又费空间 —— 在抛出异常时这或 许并不重要,但出于连续跟踪的考虑,应尽量避免。

Gail 检查了 ObserveThrownContext。它的独特之处是获取联结点上下文以 用 于日志消息中,如清单 7 所示:

清单 7. 添加联结点上下文

after() throwing (Throwable thrown) : observe() { if (observingThrown(thrown)) {  // log or ??, including join point context  observeException(thrown, thisJoinPoint);   registerThrown(thrown); }}

Connor 对类层次结构的思考

Connor 希望将大部分 ObserveThrownContext 和 ObserveThrown 的实现置 于 一个通用的超类 ThrownObserver 中。这有利于得到一种 Gail 可理解的小方面 。

但 Connor 怀疑 ObserveThrownContext 是否能够扩展 ObserveThrown 或者 反之。您对他解释,具体类只能扩展抽象类。此外也没有什么重写通知的好方法 。有时您可以将通知重构为方法,但在本例中,需要特殊形式的 thisJoinPoint ,因此无法实现。Zed 在实践中得到了这样的结论:如果超方面中包含通知,那 么仅能提供一到两个超方面层。

多亏 Gail 及时插话,才使 Zed 未得出危险的结论。她指出这组方面足以完 成大多数普通的日志记录,惟一不能处理的就是对信息在联结点不可用处的特殊 日志记录。有了方面,日志记录比以前更易管理,而在此之前,她必须要进行大 范围的更改。

Holly 坚持缓存

Holly 是一个什么东西都不肯扔掉的人,她把每样东西都收得妥妥贴贴,觉 得 总有一天会用到。她也是一名出色的 Java 程序员,因此 Zed 要求她尝试缓存 不 同的内容,以观察对性能的影响。在与她交谈后,您提供了 3 种用于缓存的方 面 ,如表 4 所示:

表 4. 缓存方面

CacheToString 保存 toString() 的结果 CacheMethodResult 按键映射任何结果 CachedItem 按一个目标保存结果

自由关联

这些缓存方面的不同之处在于值与缓存的关联方式:利用切入点、方面实例 化 与映射的某种结合。

CacheToString 与之前提到的方面相似,要求 Holly 编写类似于 within (com.magickingdom..*) 的 scope 切入点。Buddy 指出 CacheToString 是一个 perthis 方面,因此方面的一个实例与任何实现 toString() 的类的各实例相关 联。缓存值存储在方面本身内,方面则仅与 toString() 方法执行进行协作。

相反,CacheMethodResult 实例化为处理任意方法的单体方面,返回给定类 型 。由于 CacheMethodResult 的子方面是与许多缓存值协作的单体,所以各方面 使 用映射将缓存值与给定联结点关联。在设计方面,任何缓存方面都涉及将逻辑置 于键内及将逻辑置于切入点和方面实例化内之间的权衡。CacheMethodResult 使 子方面能够重写切入点和为结果创建键的方法,因此 Holly 可在顾及其程序的 情 况下做出权衡。在一种情况下,切入点仅可辨别出一种静态方法,这也就意味着 在实现键时,仅有一种参数是相关的。另外一种子方面切入点可辨别不同对象的 多种实例方法,使目标对象和方法签名也与键相关。

在尝试缓存时,Holly 非常喜爱 CacheMethodResult 的一般性,但她也希望 利用切入点获得更紧密的 CacheToString 关联。若每次构建一个键并使用映射 , 代价将非常高昂。在调试时,通过子方面类型即可明确缓存了哪些内容,但必须 解释 CacheMethodResult 复合键。然而,CacheMethodResult 将自己的范围限 于 方法结果,从而使子方面编写程序可更容易地编写切入点。因此它无法直接缓存 字段值。

有了 CachedItem,Holly 就可以在获取字段、从方法或构造器调用处返回值 时缓存任何内容。但方面没有映射,而是假定切入点可准确地识别值。若编写了 错误的切入点,就会导致两个值相混合。

Holly 非常喜爱缓存方面,但这些方面均要求以手动方式使缓存无效,这让 她 不太满意。Zed 也对此不满。尽管有无缓存,系统都可运行,但只要有依靠缓存 方面清除缓存的地方存在,缓存方面就不可移除。

追求平静的 Irene 实现幂等方法

Irene 不喜欢变化,也不喜欢为之努力抗争。与 Holly 一样,她也希望提高 性能。她希望您能帮助她回避幂等(Idempotent) 方法,若多次使用这些方法 ( 例如,在打开已开放的资源或关闭已关闭的资源时),则不会产生任何效果。您 编写了两个方面,均可跳过已运行的方法。两者在关联方面有所不同,与缓存类 似。

表 5. Idempotent 方法管理方面

IdempotentMethod 特定方法的 pertarget IdempotentMethods 按各方法映射键

这些方面使用各方法上的注释,Buddy 对此感到非常兴奋。Iren 希望将代码 中的方法标识为幂等,以使开发人员得知不应对该属性进行更改。若她在一个切 入点中枚举了方法,则他人可能会更改或重命名方法。有了注释,所有人都会得 到通知,保留其幂等性。系统可在不需任何人更新方面的情况下实现更改,Zed 对此表示满意。

Holly 再寻基于注释的缓存

Holly 依然对缓存方面不够满意,又回来找您。如果缓存项目以期待的生存 期 注释自己,又该怎么办呢?例如,产品描述过时 5 分钟无关紧要,但拍卖价格 必 须准确到分,乃至秒。若缓存方面可读取本信息,就可在指定时期后使自己的缓 存失效。

您告诉她这是可行的。这里有两个标记了其结果生存期的方法:

清单 8. 生存期注释

@TimeToLive(300)public String getName() // ...@TimeToLive(100, TimeUnit.MILLISECONDS)public Price getPrice() // ...

清单 9 展示了使用注释值确定何时清除缓存的 TimedCacheItem 方面:

清单 9. 在生存期后清除缓存

Result around(TimeToLive ttl) : @annotation(ttl) && results() {  Result result;  // long nanoBirthTime set when cached  if (0 != nanoBirthTime) {   // calc time to clear cache from annotation duration   long lifeTime = ttl.timeunit ().toNanos(ttl.value());    long deathTime = nanoBirthTime + lifeTime;    if (deathTime < System.nanoTime()) {      clear();    }  }  // ...

这个解决方案得到了 Zed 的认同。若方面被移除,缓存也随之清除,但没有 其他任何东西负责使方面缓存失效。此外,最好由方法开发人员去估计缓存期。

所有人看上去都很满意,但 Connor 突然提议实现更高的灵活性。若存在两 个 访问点,则同一个值拥有另外一种不同的生存期是完全可能的。例如,参与拍卖 的人希望为其价格使用短期点,系统管理员则希望使用长期点来计算总和(因为 细微的价格误差不会影响总和)。采取这种方式也就意味着总和通常不会清除缓 存,且会降低系统运行速度。Connor 的提议形如清单 10 所示:

清单 10. 按访问区别的生存期

@TimeToLive(100, TimeUnit.MILLISECONDS)public Price getPrice() // ... @TimeToLive(5, TimeUnit.MINUTES)public Price getPriceForSummary() // ...

Jodi 的决断就是常量注释

Jodi 天生眼光敏锐、严格苛刻:她总是希望了解事情有无变化。她特别擅长 多线程编程,一直希望 Java 语言中有 C 语言的关键字 const,在多线程代码 中 ,const 函数不会导致任何转变,因此也不必为访问该函数而忧虑。为满足 Jodi 的需求,您提供了 Const 方面。若试图修改标记为只读的字段或者是标明为只 读 的方法或类,试图访问非可读内容,它就会报错。Zed 非常喜欢这种思想,但认 为它不会太有用。然而,由于它们仅仅是注释,并且方面中仅有错误声明语句, 所以该方面是无害的。

为处理确实发生了变化的状态,Jodi 希望能实现一个版本号,若在上一次读 取状态后,状态发生了变化,则版本号将简化对客户机的通知。针对这种需求, 您提供了一个具体方面 Versioning。Jodi 无需编写切入点或注释,她可以通过 声明目标类型来实现 IVersioned,如清单 11 所示:

清单 11. IVersioned 接口

public interface IVersioned { int getVersion();}

Versioning 方面处理实现,API 客户机直接使用版本号。Zed 反对编译必须 使用 Versioning,这样就无法将其从系统中移除,也无法在装入时织入中使用 。 Jodi 表示将与 Holly 一起工作,观察该方面是否可用于缓存;并将进行试验, 观察该方面是否有助于避免在多线程程序中使用锁。AspectJ 至少有助于完成试 验,即便最终实现直接以代码编写。

Kelli 跟踪事情的状态

Kelli 是开发专家之一,测试部门向她抱怨说,出现了大量因未遵循协议而 出 现的故障。为及时检测到协议中的无效步骤,Kelli 希望为组件或子系统维护状 态模型。她向您描述了一个简单的资源模型:该模型必须在写入之前打开、在打 开之后关闭,且在关闭后不得再打开或写入。您为她提供了两个方面,如表 6 所 示:

表 6. 跟踪方面

TrackedNames 将名称与各联结点相关联,提交给可插拔跟踪程序 TrackedMethods 扩展 TrackedNames,从文件中读取允许的状态转换,并进行失效实 时 处理

TrackedNames 将联结点名称作为转换,并向委托 ITracker 查询转换是否有 效。跟踪程序维护所有必需的逻辑和状态。可用的两个 ITracker 是 TrackedSequence 和 StateTracker。

图 3. 跟踪类关系

TrackedSequence 以正则表达式的形式表示有效名称序列。例如,${open} ${write}*${close}. StateTracker 从文件中读取状态转换,在本例中的形式如 下:

清单 12. 资源状态转换

STARTSTART open OPENOPEN write OPENOPEN close CLOSE

在 TrackedMethods 和 TrackedNames 之间,Kelli 倾向于前者,因为她喜 欢 将转换作为方法名称定义;在 StateTracker 和 TrackedSequence 之间,Kelli 倾向于前者,因为它可在无效转换发生时立即检测到。总体来说,她使用 TrackedMethods 扩展 TrackedNames,使用 StateTracker 在错误步骤发生时及 时检测。

Buddy 指出,有了缓存的键映射方法,Kelli 可以将 getName() 重写为重新 映射名称,而不必使用联结点名称。Zed 喜欢文件的转换表形式,因为它还可用 于其他方面,例如生成测试案例的完整集合等。Connor 认为这不仅仅可用于跟 踪 ,也可用于实现复杂的协议,例如,将一系列资源写入操作打包到一个转换内。

Liz 爱玩弄并发

Liz 曾靠在业余时间表演魔术而读完大学,她至今仍保留着一颗顽皮的心, 这 非常适合她的研究职位。Zed 要求她就并发做一些实验,尝试提高系统的响应性 。Liz 对 Gail 略有不满,因为 Gail 近来获得的能力使其可将一切内容记录入 日志,导致系统速度变慢。她们已经尝试使用了过滤器,但无论是 Liz 还是 Gail 都不愿为时间而放弃信息。

您提供了 SpawnTrueVoids,将指定空方法重定向到在另一个线程内运行的工 作队列。在 Connor 的协助下,Liz 编写了 SpawnLogging,扩展 SpawnTrueVoids 以将空日志记录调用排序到另外一个线程内。不幸的是,至少 在 日志记录线程内存在需要依序运行的非空方法。Kelli 提出了两种模式,用于日 志记录程序的初始化(无空日志调用,除某些正在变化的调用外)和日志记录( 仅空日志调用,迅速被带入单独线程中的队列)。然而,Liz 希望保留在运行时 通过 JMX 配置日志记录程序的能力。

Zed 得出结论,显式并发非常困难,所以使其为粗心的客户机所用更是难上 加 难。但在开发时,可出于调试的目的使用显式并发完成额外的跟踪工作。将来, Liz 和 Kelli 还可尝试更多的解决方案。

并行方法执行

关于实验,Liz 还有另外一种想法。她希望试试并发重构的另外一种方法, 但 已厌倦了包含于打包代码中的样本,也厌倦了线程调用。如果使用注释声明一种 方法为 “并行的”,会怎样呢?方法中的所有代码都可并发执行。

所有人都投入这项工作中。最后,ParallelMethodImpl 合并了多种惯用特性 和新特性。与 TimedCachedItem 相似,它使用了一个注释 (ParallelMethod) 来 标识并行方法。

至于状态关联,ParallelMethodImpl 将为各并行方法的方法执行维护一个 Future 列表,表示产生的方法调用的未来可能结果。由于这一未来结果集合特 定 于并行方法的给定调用,因此为并行方法执行的各控制流实例化一个 ParallelMethodImpl,如清单 13 所示:

清单 13. ParallelMethodImpl 实例化

public aspect ParallelMethodImpl percflow(execution (@ParallelMethod * *(..))) {

实现并行方法

通知本身相当简单,只是防护和调用。通知首先检查执行器(在线程内运行 代 码的 Java 5 工具)。若执行器不可用,通知将同步启动。否则将创建并运行 Future。

清单 14. 实现并行方法

void around() : withincode(@ParallelMethod * *(..)) && call(void *(..)) {ExecuTor execuTor = getExecuTor();if (null == execuTor) {proceed();} else {FutureTask future = new FutureTask(new Callable() {public Object call() {proceed();return DONE;}});futures.add(future);execuTor.execute(future);}}

Connor 详述了实例化与配置之间的不匹配。对于实例化,方法的每个控制流 都有一个方面。对于配置,方面的所有实例都使用同一个 ExecuTorService。 Liz 按 Java 语言中的可行方法解决了此问题:ParallelMethodImpl 中的静态 方法获取方面实例所使用的工厂以在创建时获得 ExecuTorService。使用工厂方 法使方面实例间可实现状态共享。

Zed 喜欢这种方法,因为开发时方面可使 Liz 的实验更简单。通过方法注释 可非常清楚地了解正在发生的情况,而且不需要在匿名 Runnable 中打包代码。 各位,干得好!

Mary 成为观察者,完成艰巨的任务

Mary 随时留心需要做的工作。她想要一种实现一流观察者协议的方面。在 AspectJ 中有多种方法可实现此协议,但您提供了 SubjectObserver,它参数化 各参与类型。各子方面表示一个给定的关系,所以两个组件之间可有多个主体 — 观察者关系,且无任何 API 冲突。关系的定义非常简单,如清单 15 所示:

清单 15. 声明主体-观察者关系

static aspect A extends SubjectObserver {protected pointcut changing() : execution(void S.go());protected void updateObserver(S s, O o) {o.going(s);}}

为应对 Zed 对取消代码(使方面更易于测试)的关注,您将大部分功能都置 于库方面超类 AbstractSubjectObserver 中。这使库的规模可与具体方面一样 小:

清单 16. 最小化的库方面

public abstract aspect SubjectObserver extends AbstractSubjectObserver { protected abstract pointcut changing(); after(Subject subject) returning : target(subject) && changing() {subjectChanged(subject);}}

客户机注册观察者时,可使用 AbstractSubjectObserver 的引用避免直接依 赖方面(尽管使用了方面,但客户机无需了解这一点!)。若取得了方面,就必 须直接调用 subjectChanged(..),而客户机不需要更新。

Zed 非常欣赏此解决方案,他甚至要求 Mary 将其作为必需的测试代码进行 实验。若无意外,Zed 将批准在产品中使用此解决方案。

Zed 组织讨论

现在您已取出了口袋中的所有方面,问题出现了:Zed 究竟能否批准方面成 为团队日常部署的一部分?他对此持几分赞成态度?Zed 请 Arnold、Buddy 和 Connor 向所有人展示他们的学习成果,通过这个机会,您可以在他们独立编写 方面之前,审查其思想的完整性和正确性。

Arnold 对切入点的理解

Arnold 一直对切入点很感兴趣,Erin 以代码审查方面标记他的代码后,他 又对 declare error 和 declare warning 语句产生了特殊兴趣。而您使用这种 机制防御性地编写库方面、在子方面切入点中标记错误,这令 Arnold 感到非常 惊讶(回想起来,又发现这非常有意义)。例如,并行方法仅可包含返回空值的 方法调用,如清单 17 所示:

清单 17. 并行方法执行

declare error : withincode(@ParallelMethod * *(..)) && !call(void *(..)): "Parallel methods contain only void method- calls";

还有另外一个例子,CacheMethodResult 假定切入点仅可辨别方法调用或方 法执行联结点,因此,若指定了任何未经许可的联结点,就会出现警告:

清单 18. 切入点声明错误

declare warning : targetPointcut() && ! permittedPointcuts() : "targetPointcut() restricted to permittedPointcuts() ";

CacheMethodResult 许可哪些内容呢?返回特定类型的方法调用或执行,如 清单 19 所示:

清单 19. 许可的结果

/** method-call or -execution returning Result (+: covariant ok) */pointcut permittedPointcuts() : execution(Result+ *(..)) || call(Result+ *(..));

记住,这里的 Result 是一个类型参数。若具体子方面指定以 String 作为 类型,则该方面仅许可返回类型为 String 的方法签名。

同样,若切入点选择的不是方法执行,若方法不返回空值,若方法接受参数 ,IdempotentMethod 就会声明错误。它使用切入点指定联结点。反之, IdempotentMethods 使用仅可应用于方法的注释,因此仅在注释错误地放置在返 回非空值或接受参数的方法中时,才需要发出警告 —— 更正注释的放置错误。 (Irene 认为这仅在验证注释时才有用)。Arnold 领会了其中的关键:只要可 能,就应向部署程序提供关于错误的织入时反馈,而不是使方面在运行时失败。

您补充,有些此类反馈是随通知一同出现的。只要通知声明它抛出异常,若 应得到通知的联结点未得到抛出异常的许可,AspectJ 工具就会发出错误信号。 同样,若得到通知的联结点无法返回 around 通知的结果,这些工具也会发出错 误信号。

尽管如此,大部分库方面至少会将部分规范委托给部署者 —— 以具体类或 目标注释、接口、类型甚至成员命名规范内定义的切入点实现控制。无法在织入 时检查到所有此类情况,因此您必须防御性地编写程序,在必要时放弃一点控制 权,以使部署者可完成其工作。这往往意味着使用模板切入点。与模板方法相似 ,模板切入点是由多个部分组成的,其中某些部分由子方面部署者编写而成,用 于根据手头的程序调整方面。流行的两种模板切入点是 Scope 模式和 Trifecta 模式。

模板切入点中的模式

Arnold 表示他已注意到您对 Scope 模式的使用。您的许多简单方面都指定 了库方面中的此类联结点,但允许部署者使用 within 或 withincode 切入点指 定所关注的类型或方法。Buddy 指出没有任何因素阻止子方面用户使用 within (..) 以外的形式,实际上,Arnold 的 volatile 异常就是使用 get(..) 和 set(..) 切入点构成的。您回答说,或许超方面编写者并不希望发生这样的事情 ,但这样做是安全的,因为只会进一步限制 —— 而非扩展核心切入点。

这是一种错误类型,选择错误,但还有其他类型的错误需要密切注意。Zed 问:“若切入点什么也没有选择,又会怎样呢?” 有些通知并非一定要匹配所 有的程序,因此也未必是错误。但很可能是一个无意的疏忽所致,所以编译器会 为通知发出警告。警告是可配置的,用户可忽略警告,若用户知道通知未运行或 不允许运行,也可将其确定为错误。

Holly 指出,缓存方面拥有一个上下文切入点,用于完成运行时类型检查和 变量绑定。您解释说,那实际上是 Trifecta 切入点模式的一部分:

表 7. Trifecta 切入点

核心 用户指定的关注联结点 许可的 指定联结点类型和任意预期静态上下文 上下文 指定动态测试和值

组合在一起,如清单 19 所示(caching() 作为核心 切入点):

清单 20. Trifecta 切入点

/** the pointcut composed from the user, as permitted, with context */pointcut results() : caching() && permitted() && context();

Trifecta 模式处理了两个问题。首先,如何检查部署者编写了功能超出指定 范围的切入点。为此,库方面编写程序指定了许可的 联结点,并编写一条错误 声明,标识部署者的切入点所选择的任何未经许可的联结点,形式如下:

清单 21. 切入点防卫

/** warn if subaspect pointcut picks out unpermitted join points */declare warning : caching() && !permitted() : "unpermitted caching()";

其次,Trifecta 模式分离了切入点的可静态确定的 部分,以在错误声明中 使用。这些声明不能接收使用运行时检查的切入点,因为这类切入点在织入时并 非一直可确定。因此,declare error 切入点中不能包含切入点 this(..)、 target(..) 或 args(..)。Trifecta 模式为部署者将它们分开放置在单独的切 入点内,从而使核心切入点可得到独立检查。Trifecta 模式是 “完美” 的, 原因不仅仅在于可将切入点分隔为 3 个部分,而且您可在 3 个地方 —— 部署 者的规范中、警告/错误语句中,以及通知本身中 —— 看到这些部分。

Holly 在她所见过的 Trifecta 切入点中观察到了这些特点,超方面通常将 核心切入点保留为抽象切入点,以强制部署者对其进行定义,但将上下文切入点 定义为空,若部署者不需要上下文切入点,而是在必要时重写它,则可忽略该切 入点。与方法类似,重写切入点可为强制的,也可为可选的,具体取决于超类型 开发人员是否认为默认实现不会导致错误。

Buddy 对注释的理解:类的标记!

Arnold 讲完后,轮到了 Buddy。他最初将 Java 5 注释视为标记,但生存期 的示例使其陷入思考。AspectJ 5 使部署者可利用 Java 5 注释选择性地使用切 入点,甚至与通知通信。Buddy 主动阅读了一些资料,指出 AspectJ 5 还允许 开发人员在方面内声明其他注释类型及其成员。因此,有两个关于在方面中使用 注释的问题:(1) 是否使用它们取代切入点或接口来指定关注;(2) 是在主体代 码中声明注释还是在方面中声明,或者是在两者中同时声明。

Buddy 正确地注意到,只有在主体联结点拥有可与注释关联的签名的运行时 中保持注释时,注释才是有用的,而这样的签名可为类型模式、字段、方法或构 造器。(标记接口同样限于类型主体。)

在确定注释与方面配合使用的方法时,Buddy 谈到注释与切入点之间有 3 点 不同。首先,注释用作程序源代码中可用的标志,标明其性质。其次,注释中可 包含在通知中用于控制行为的状态或代码(例如,在缓存示例中,注释中包含生 存期;在 Sun 的示例中,要在运行方法之前运行测试代码)。第三,注释可在 代码发生变化时更改,而不会影响到方面 —— 在使用库方面时,这是一个非常 重要的考虑事项,因为它在某种程度上实现了灵活性,不要求方面本身得到更新 。而对于像 Holly 这样期望以个案为基础添加新切入点主体的开发人员来说, 最后一点尤为重要。

为迅速地将问题转到注释的声明位置上,Buddy 提出以个案为基础添加大量 主体的一种方法是编写另外一个方面,声明结合新主体的一组注释。每个人都赞 同这种做法,但您提醒大家注意语言问题。通常,注释是一种针对特定领域的语 言的一部分,如事务或缓存。当开发人员需要代码环境中的提示时(例如,我们 前面遇到的幂等方法),注释可与受影响的成员相关;但若工具作为原则消费者 (如事务),则最好将规范合并在方面之中。

Buddy 表示可以混合声明类型,在代码中声明一些,再在方面中声明另外一 些。此外,编写库方面时,开发人员不必选定策略 —— 除非他们只希望由方面 来声明注释,此时可声明方面私有的注释。在任何情况下,这都是一个语言难题 :与其说问题在于选用 AspectJ 还是 Java 的问题,不如说是用户能否理解由 注释声明构成的 小型语言 是由方面(或其他工具)实现的。

在注释含有可引导通知行为的状态或代码时 —— 例如,在特定时间后使缓 存失效或运行测试代码,就体现出了注释最强大的用途。在解释程序通知解释或 调用注释时,注释数据/代码状态的表达力会受到一定限制,特别是在与联结点 状态结合时更是如此。务必谨慎,确保用户能够理解事情的来龙去脉。

Connor 将类型与联结点联系在一起

Connor 起初最关心的是集成方面,但他逐渐发现同时在通知参数或类型间声 明 (ITD) 和切入点内指定一种关注类型并不方便。若类型或切入点发生变化, 您就务必更改参数或 ITD,这增加了维护的难度。

还好,AspectJ 5 以通用方面、带有类型参数的抽象方面部分地解决了此类 问题。类型参数可用在切入点和成员声明中(但不能用在类型间声明中,至少现 在还是这样)。最佳范例就是 CacheMethodResult。此方面使用 Result 参数化 ,切入点和缓存值都按此方式指定。开发人员在具体方面中指定类型参数(例如 ,指定为 File),此后切入点仅选择返回 File 的方法,且映射仅接受类型值 File。这也就意味着从结构上修正了方面,而不必再检查不变量。

关于方面的合并,Connor 提到有些极为出色的库方面实际上非常小,尤其是 那些将纯粹的 Java 代码实现于一个超类之中的库方面更是如此,而超类可独立 实例化、独立测试。(这里他给出的例子是 ThrownObserver 和 AbstractSubjectObserver。)Holly 和 Kelli 对缓存与版本控制的合并非常感 兴趣。每个方面实现部分解决方案,而不是直接将方面放在一起使用,这样各部 分都可以在其他解决方案中使用。与 Java 中的情况一样,通用公共接口有助于 避免使实现的各部分了解过多关于其他部分的信息。无论对于注释来说,还是对 于由关注主体定义的公共切入点来所,这都是可以实现的。

Zed 需要定论……

Zed 要求您提供一份考虑事项一览表。下面就是您为他提供的:

1.方面用于产品还是开发?降低风险!

2.方面是可选的还是必备的(例如 ,必须使用方面才能完成编译)?必要程度如何?如果方面发生故障或被删除会 怎样?

3.需要什么才能限定一个库方面?

1.无

2.可选切入点

3.简单 的 within 切入点

4.完全量身订做的切入点

5.应用接口或注释

1.在目 标代码中声明的接口或注释

2.在方面中声明的接口或注释

3.同时在目标代 码和方面中声明的接口或注释

6.对于注释或接口,声明是否不仅影响切入点 ,还要影响通知?

7.重写方面方法

8.配置方面委托(例如,通过工厂配置 )

4.方面的风格

1.代码风格:AspectJ 对 Java 语言的扩展

2.注释风 格:以 Java 注释声明方面(方面必须为可选的)

3.XML 风格 (?):在 aop.xml 中声明具体方面(方面必须为可选的,且库方面中仅切入点可为抽象的 )

5.部署问题

1.装入时织入(仅对可选方面有效)

2.装入时织入平台 变量(多重)

1.Java 1.3:自定义类加载程序,基于 WeavingURLClassLoader

2.Java 1.4:AspectJ 1.2.1 aj.bat 脚本,取代系 统类加载程序

3.Java 5:Java 5 装入时字节码织入器挂钩

6.理解库方面

1.核心组件

■切入点

■通知

■类型间成员声明

■类型间父声明

■类型间错误声明

■要重写的受保护方法

■配置挂钩

2.numericity :方面实例化、联合、映射及切入点

3.类型安全:通知约束、mixin 接口、 AspectJ 5 参数化类型和方面

4.敏感度:明确;小型语言?

5.可维护性

1.可理解性

2.是在方面中还是在程序中跟踪程序变化?如何通知?

3. 检测错误:

■AspectJ 工具错误和 lint 消息

■方面声明错误消息

■ 测试代码

■运行时不变量测试(产品代码中的失效实时处理)

■(见以下 关于健壮性的部分)

7.健壮性:系统发生变化时是否依然正常工作?

1.切 入点中有哪些假设?

1.这些假设有编译时测试的保障吗?

2.它们采用了严 密、防御性的编码方式吗?

3.切入点未被重写时,有通知吗?被重写时呢?

4.它们采用了严密、防御性的编码方式吗?

5.用于在抽象类中构成切入点 的 Scope 和 Trifecta 模板

2.系统中的新子类型会带来什么影响?

1.相 同的切入点或注释对它们起作用吗?

2.它们遵循同样的环境假设吗?同样的 异常处理?所有子类型都应按超类型处理吗?

看到这份一览表后,Zed 高兴 地吻了您一下!感觉很怪异,但这不失为一个好兆头。

库方面一览

库方面挑战游戏结束了,通过这个游戏简单介绍了简化库方面编写和部署的 方面库及 AspectJ 5 特性。表 8 列出了本文中介绍的方面,可通过下载源文件 获得这些方面。表 8 中说明了:

方面是必需的 (req) 还是可选的 (opt)

方面是用于开发 (dev) 还是用于 产品 (pro)

主题范围(兼所在文件夹)

指定具体方面需要的条件,即:

within 表示形如 within(com.magickingdom..*) 的简单切入点

pointcut 表示其他一些切入点

template 表示存在允许子方面或外部客户机实现某些配 置的模板算法

config 表示有一些数据驱动的配置

generic 表示泛型类型 参数

convention 表示编码规范

! 表示经过错误声明检验

简要描述

查看源代码时,读者会看到相应的注解:

// CODE aspect opt dev topic [specification..]: description

运行 Eclipse 的读者可将 Java 编译器任务标记器设置为选择 CODE 注解, 以快速查找可用库方面。Grep 可在不使用 Eclipse 的情况下实现类似功能。

在编写这些方面时,大多以示例特殊语言特性为目的。AspectJ 小组预测将 可通过多种途径获得这些库 —— 直接由我们提供,或由 AspectJ 社区内的独 立开发人员提供。您可以访问 AspectJ 主页,寻找最新的库代码,若有任何问 题,也可 直接与我联系。希望您编码愉快,部署愉快!

表 8. 库方面一览

库方面 CacheMethodResult opt pro caching generic pointcut! 缓存方法结果,按上下文实现键 CacheToString opt pro caching {pointcut} 缓存 toString,手动清除 CachedItem opt pro caching generic pointcut 缓存需手动清除的 Result 生成器 Const opt dev invariants annotation const 方法、字段和类的错误 EqualsBoilerplate opt pro lang within equals() 空值样本代码 GetterSetter opt dev invariants within convention! 若在 getter-setter 之外执行 get-set 操作则出错 IdempotentMethod opt dev invariants {pointcut} convention! {annotation} 执行及实现幂等方法 IdempotentMethods opt dev invariants {pointcut} convention! 幂等方法(使用注释) InstanceFieldNaming opt dev invariants within 要求实例字段名以 “f” 开头 NoCallsIntoTestFromOutside opt dev invariants within convention 若非测试代码对测试代码引用则报错 NoNullParameters opt pro invariants within 遇空公共参数抛出异常 NoSystemOut opt dev invariants {within} 若使用 System.[out|err] 则报错 ObserveThrown opt pro errors pointcut template 将所抛出的不带上下文异常记入日志,但会消除重复项 ObserveThrownContext opt pro errors {pointcut} 将有 JoinPoint 上下文的异常记入日志 ParallelMethodImpl nec pro concurrent annotation convention! {config} 方法中的并行调用 RethrowThreadDeath opt pro invariants 从不捕获 ThreadDeath RethrowVMError opt pro invariants 从不捕获 VirtualMachineError+ SpawnLogging opt pro concurrent within 为提高性能而产生空日志记录调用 SpawnMutatedLogging opt pro concurrent within 将日志记录重定向到另外一个线程 —— 如何为非空调用而延迟? SpawnTrueVoids opt pro concurrent pointcut 产生没有副作用的空方法 SubjectObserver req pro patterns generic pointcut template 主体-观察者协议 SystemStreamsToLog opt pro logging within template 将 System.[out|err] 重定向到日志记录程序 ThrownObserver opt pro errors pointcut template 超类,将所抛出的异常记入日志,避免出现重复 TimedCachedItem opt pro caching generic pointcut annotation 带有生存期注释的 CachedItem TrackedMethods opt pro invariants template pointcut 使用 FSM StateTracker 跟踪方法调用 TrackedNames opt pro invariants pointcut! config template 使用可插拔跟踪程序按名称执行 FSM TrimInputStreamRead opt pro io within 将输入流读取调整为可用 UtilityClassEnforced opt dev invariants annotation 若可构造实用工具类则报错 Versioning opt pro patterns tag IVersioned 类的版本计数器

代码下载:http://www.ibm.com/developerworks/cn/java/j- aopwork14/

成功是什么?就是走过了所有通向失败的路.只剩下一条路.那就是成功的路.

AOP@Work:使用AspectJ5检验库方面

相关文章:

你感兴趣的文章:

标签云: