写点什么

Groovy 无痛 AOP 之旅

  • 2007-10-14
  • 本文字数:5378 字

    阅读完需:约 18 分钟

Cedric Beust 将面向方面编程(AOP)描述为“保留少数开发内行特权的一个绝佳主意”。对一些人来说,即便使用Spring 和JBoss,入门的门槛依旧太高。幸运的是,这正是动态语言能够给予帮助的领域。它们为闯向AspectJ 这座红灯之前的实验和学习提供了一个缓和的练习坡地,而且也依靠它们自己能力提供了一个高生产率的工作环境。Java 开发者甚至无需离家太远就可得到它们。Groovy,一门有着与Java 类似语法的JVM 动态语言,突变出令人难忘的、可以轻而易举地模拟AOP 行为的强大特性。尽管在本文中我们关注于Groovy,但是时代精神要求与非常受人爱戴和敬畏的Ruby 进行对比。Ruby 使用者可以只付出20% 的汗水就可得到80% 的成果。

AOP 允许我们模块化那些可能与众多方法和类纠缠在一起的代码。例如,我们可能发现自己在围绕(around)众多不同的工作单元进行相同的安全检查,在其它单元之前(before)实例化一些变量,在另外一些单元完成之后(after)记录一些信息,或者抛出一个异常(exception)。AOP 使得我们一次书写全部这些功能,然后在合适点(before、after、around 或当异常被抛出时)将它应用于我们的方法,这些点是那些我们曾经违反 DRY 原则(Don’t repeat yourself,不要重复你自己)并多次书写它的地方。在 Java 中,我们一般有两种机制选择。其一,我们可以使用一个框架来修改类的字节码,直接插入这些功能;其二,我们为类动态创建一个代理(使用 JDK 的动态代理或 CGlib)。在 Groovy 中,正如我们将看到的,哪一种技术都不需要。

MOPs:不只是为了干净!

Groovy 的元对象协议 是一种可被应用开发者用来修改语言实现和行为的机制。Groovy 的 MOP 特别强大。明智设计的结果是:通过 invokeMethod 调用所有的动态方法,通过 getProperty 和 setProperty 访问属性。这样,我们就有一个单一的联系点用来修改我们所创建对象的核心行为。通过覆盖 invokeMethod、getProperty 和 setProperty 我们可以拦截每个对我们对象的单个调用,无需代理或字节码处理。

解放函数!

“愿不愿意用这么多苟活的日子去换一个机会,仅有的一个机会!那就是回到战场,告诉敌人,他们也许能夺走我们的生命,但是,他们永远夺不走我们的自由!”——在《勇敢的心》中,梅尔·吉布森如此鼓舞苏格兰人。

语言迷 Steve Yegge,幽默地给 Java 标上“名词的王国”,这是个函数被奴役而且只能依附名词行走的地方。Groovy 将函数从被对象束缚的暴政中解放出来。一个 Groovy 闭包是一个匿名函数,它可访问包含它的范围,可随意被重复调用,并可将其当作数据到处传递。从语法上看,闭包定义非常象 Java 代码块。下例打印出“hello world from Groovy!”。

String printMe = "hello world"<br></br>def c = { parameter | println(printMe + parameter} }<br></br> c(" from Groovy!")创建如此轻量级函数的能力结合 Groovy MOP 的力量,使我们只需极少代码就能实现若干 AOP 风格的魔术。

开始之前

如果你想完成下面的例子,你需要遵循以下步骤:

  1. 下载 Groovy:
    可从 Groovy 官方网站下载 Groovy
  2. 遵循 Groovy 安装页面的安装指导
  3. 可选,下载并安装 Groovy Eclipse 插件。插件的更新站点是: http://dist.codehaus.org/groovy/distributions/update/ 这里有一个包含额外信息的 Wiki 站点。Groovy Eclipse 插件仍处于预发布阶段,但是,进行了一些粗略的修理,我发现它工作得还不错。你可能需要考虑下列步骤::-
  • 设置你的 step filters:在 preferences/java/debug/stepfiltering,为下列类打开 step filtering:-

    Groovy 在幕后完成了许多工作,这是为了防止了这个 Eclipse 插件在你执行代码时带你穿梭于 Groovy 基础设施之间。

  • 你应单步进入(step into)闭包而不是越过(step over)它们

  • 你目前不能直接使用 debug as/groovy,作为替代方法,你必须创建一个新的应用调试配置。这些步骤定义在 wiki 中。

  • 为了让插件去发现你的源代码,你需要给 Eclipse 一些额外的线索。一种变通方法是复制你的类目录,并将它作为最后的类文件夹加入。然后在 project/properties/libraries 中,你可以将你的源代码附加到你的哑类文件夹上。

迭代 1:启程

AOP 允许我们在指定的方法被调用时执行任意的函数。当我们这样做时,我们就被认为是应用了“advice”。在 Groovy 中,可以非常容易地实现 before 和 after advice 类型。

定义基础设施

首先,让我们书写一个 Groovy 类,它包含要被拦截方法调用的细节。我们将把这个对象作为参数传递给我们所有的 advice 实现。这会让它们“理解”它们的上下文,访问甚至修改目标对象以及入参。

请注意,关键字“def”在这儿作为一个实例变量的修饰符使用,指明变量是属性。Groovy 将为我们增加标准的 JavaBean Getters 和 Setters。同时也要注意这个类继承了 Expando,因为这个在后面很重要。

我们可以定义一个类,它覆盖 invokeMethod 并插入对我们的 before 和 after advice 类型的可选调用。我们只需要从这个类继承就可利用类 AOP 的功能。为了强制 Groovy 为我们类的每个方法调用去调用 invokeMethod,我们需要实现标记接口 GroovyInterceptable。

在这个类中关键字 def 被作为局部变量修饰符使用。这说明这个变量是动态类型。我们也利用了 Groovy 所支持的非常方便的参数命名来实例化 AopDetails 对象。

这个原则可被扩展到属性访问,但是为了清楚起见,我将让你来想象这个实现。

我们已经定义了启程必须的所有基础设施,那么让我们使用一个例子来示范一些实际的 AOP。

一个实战例子

我们需要一个 main 方法将 advice 应用到我们的 Example 类,并打印出一些有帮助的状态消息。

如果旅程中有 Groovy Eclipse 插件一起陪伴你,那么你可以在 main 方法的开始设置一个断点,并跟踪进入这个代码。在文件上点击右键并从弹出菜单选择 run-as -> groovy,我们可以执行 Runner.groovy,如果想要调试,可以按照上述 wiki 中的步骤。Runner.groovy 执行后输出如下:

Calling print message with no AOP :-<p>Calling print message with AOP before advice to set message :-</p><br></br>hello world<p>Calling print message with AOP after advice to output status :-</p><br></br>hello world<br></br>Status:message displayed<p>Calling print message via invokeMethod on a Statically defined Object :-</p><br></br>hello world via invokeMethod最初,我们使用“message”键的值为空字符串的 Map 调用 printMessage。但是,在后来的 printMessage 调用之前,我们声明了一个被应用于 printMessage 的 before advice,它修改“message”键的值为“hello world”。接着,我们应用一个 after advice 来输出状态消息。我们可以在 invokeMethod 中设置断点清楚地看到所发生的一切。

“invokeMethod”在 before 和 after Map 中查找一个使用当前方法名为键值的闭包去执行。如果找到一个——它就在合适点执行它。

这个小例子示范了能被容易应用在更复杂、现实世界条件下的原则。

迭代 2:绕着我们走

到目前为止,都很直接。对于接下来的迭代,我们将加入 around advice。around advice 是更强大的 advice 类型,在其中它可以修改应用的流程——它有权决定我们的目标方法是否被执行。这样的话,它是一个实现安全特性的绝佳地点。

定义基础设施

我们需要修改我们的基础设施以配合新的 Advice 类型。我们无需改变 AopDetails。在设计 AopDetails 时,我们使用 Expando 作为基类。Expando 是一个特殊的 Groovy 对象,它利用了类似 MOP 的伎俩提供一个可扩张的对象。我们需要给 AopDetails 增加 proceed(),这样我们的 Around advice 就可以调用目标方法,使用 Expando,这不是问题。让我们看看新的 AopObject:

我们增加了一个 map 来包含我们的 around advice 闭包。continueWith 闭包调用目标方法,我们临时将它作为 proceed 方法加入到 AopDetails 对象。如果存在合适的 around advice,我们就调用它;否则,我们继续直接调用目标方法。

另一个实战例子

这次,让我们的例子更加现实一些。我们将试图模拟 Web 控制器。

我们的控制器实现了一个“save”方法,使用强大的 Groovy 特性(Expando 和闭包),我们模拟出一个模型对象。我们给模型增加了两个函数——params,从 request 加载参数;save,“保存”模型。因为 save 动作可能执行一个敏感的更新,围绕它引入一些安全特性是一个好主意。

这个 Runner 类使用 Map 模拟出 Request 和 Session 对象。最初,我们在 controller 上不使用任何安全特性调用 save,但是在随后的调用中,我们插入了检查保存在 session 中的用户 id 的 around advice。最后,我们示范了如果用户 id 正确,初始方法就可以真正处理。输出如下: -

Attempting initial save : no AOP security:-<br></br>Saved model 10 successfully <p>Attempting save with AOP security & incorrect id:-</p><br></br>Sorry you do not have permission to save this model (10) <p>Attempting save with AOP security & correct id:-</p><br></br>Saved model 10 successfully截屏显示了处于 Runner.groovy 中的 around 安全闭包中的断点的调试器。

迭代 3:可重用方法

使用闭包,我们可定义独立于类的方法。使用 MOP,正如我们使用 Expando 时已看到的,我们可以将这些自由方法插入多个类。哎呀,我们甚至可以把不同的方法注入相同类的不同对象。这种模块性级别比标准 Java 允许的粒度要细的多。我们大多数 Java 开发者习惯通过对象接口实现来注入服务,而且我们一般使用大量的 XML 来达到这个目的。

定义基础设施

我们可以扩展我们的类 AOP 基础设施类,以允许插入和删除来自我们对象的方法。在继承层次的每个端点,我们需要两个新类来完成这个。AopIntroductions,它允许我们给我们的对象引入新方法,成为我们的基类(AopObject 现在需要继承 AopIntroductions)。AopRemovals,它阻止指定方法的调用被执行,是一个新的顶级类。

开始游戏!

现在我们有了真正的有趣的东西!我们能够利用实例方法作为可重用组件。我们可以在一个地方定义通用的方法,然后将它们混入跨越多个类类型的对象,同时限制对每个对象为基础的敏感方法的访问。

让我们从前面的例子中通过移除 save 方法来重构我们的控制器。我们将创建一个 CrudOperations 类,它将用于包含 CRUD 方法。接着,我们将这些来自我们的 CrudOperations 的方法注入到我们的 Web 控制器。

注意对 CrudOperationsMixin 构造函数的调用,这就是魔法发生之地!

在以上类中,我们定义了一个方法引用的 Map。我们通过方法对应的键将那些引用注入 host 对象(我们的 Web 控制器)。

这种方式的美妙之处在于我们仍然可以给新加入的方法应用 around、before 和 after advice 类型!来自前例的 Runner.groovy 可以如平时一样运行。但是,我们将加入另一种手法,在新类运行时不让 save 方法运行。

Groovy 支持操作符重载,以及 List 类使用左移操作符(“<<”)给 List 添加新对象。

运行 Runner.groovy 的输出如下:

Attempting initial save : no AOP security:-<br></br>save operation <br></br>Saved model 10 successfully<p>Attempting save with AOP security & incorrect id:- </p><br></br>Sorry you do not have permission to save this model (10) <p>Attempting save with AOP security & correct id:-</p><br></br>save operation<br></br>Saved model 10 successfully <p>Attempting save with save method removed (perhaps for security reasons):- </p><br></br>Caught NoSuchMethodException在下面调试器截屏中,我已经加亮了显示 Web 控制器路径的堆栈轨迹(StackTrace),对 save 的调用部分,实际是在实际包含 save 方法的 mixins 类中完成的。

从 Groovy 到 Ruby:火车司机的备选

如果 Groovy 不合你的胃口,我们可以在你选择的动态语言中重新实现以上各例。例如在 Ruby 中,我们可以覆盖 method_added 钩子而不是 invokeMethod。“method_added”在新方法被加入到一个类时被调用。在方法被加入到我们的对象时,我们可以代理它们,以一个经由 alias_method 插入 before、after、around advice 的实现将它们交换出去。即便是曾经祸害过每个开发者生活的 Javascript,也有强大的方言能够方便地实现 AOP。甚至还有专门的框架——AspectJs!

总结

象 Groovy 这样的动态语言可以无痛地实现自定义的 AOP。这不需要字节码修改,基础设施代码库可以保持短小,使新开发者容易理解。Groovy 的元对象协议允许我们重塑对象的行为以适合手头的问题,顶级函数和动态类型使得动态插入功能相对容易。本文描述的这些例子是迈向 AOP 全功能旅程的第一步。尚未实现的是 exception advice,对每个方法多 advice 支持(包括 around 的 advice 链)和应用 Aspect 到对象的集中化支持。但是,我希望我已经示范了:只需相对极少的努力,我们可以走得非常远,这都要感谢 Groovy 语言强大的能力。

资源

  • Nothing new under the sun, :around, :before and :after in

    CLOS 1988.

关于作者

John McClean 是居住于爱尔兰都柏林的软件开发者,他在那儿为爱尔兰的 AOL Technologies 工作。在他的空闲时间,他开发了 Slingshot Application Framework ,它是一个高效率的基于 Web 的应用的生产环境。John 维护一个博客,用于记录他对于感兴趣技术主题的想法。

查看英文原文: Painless AOP with Groovy

2007-10-14 21:494344
用户头像

发布了 255 篇内容, 共 56.6 次阅读, 收获喜欢 10 次。

关注

评论

发布
暂无评论
发现更多内容

C语言:十进制、BCD码互换

不脱发的程序猿

C语言 十进制、BCD码互换

【Flutter 专题】59 图解 Android Native 获取 Flutter 资源文件

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 9月日更

cxx 的枚举类型

hedzr

c++ 算法 枚举

Linux之lastb命令

入门小站

Linux

JDK 8 及其后续 JDK 中 Period 和 Duration

HoneyMoose

Vue进阶(八十九):watch 用法详解

No Silver Bullet

Vue 9月日更

打分吧!客服小姐姐,评分页面与客户总分页面的 Django 实现

梦想橡皮擦

9月日更

Java中高级核心知识全面解析,Java工程师需要掌握的技能

Java 程序员 后端

【LeetCode】所有奇数长度子数组的和Java题解

Albert

算法 LeetCode 9月日更

网络攻防学习笔记 Day123

穿过生命散发芬芳

9月日更 互联网安全 流量采集

前端开发css这些样式你还熟悉吗,Chrome是必备技能

你好bk

CSS html css3 大前端

JavaScript数组常用的方法总结

孙叫兽

JavaScript 大前端 数组 引航计划

IntelliJ IDEA 如何快速查看提交代码的对比

HoneyMoose

活动回顾 | Apache Hudi x Pulsar Meetup 杭州站(戳进看视频)

Apache Pulsar

阿里云 Apache Pulsar StreamNative

epoll底层实现源码及epoll反应堆模型

hanaper

linux网络包收发讲解

赖猫

Linux

Go 专栏|开发环境搭建以及开发工具 VS Code 配置

AlwaysBeta

Go 语言

08. 语音识别与第二次AI热潮

Databri_AI

人工智能

找啊找啊找工长,找到一个好工长

escray

生活记录 9月日更

在线JSON转Mongoose工具

入门小站

工具

Excelize 开源五周年 🎉

xuri

Excel Excel数据分析 Go 语言 Excelize

LeetCode题解:143. 重排链表,数组,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

C语言:十进制、十六进制数据互换

不脱发的程序猿

C'语言 进制转换

看懂这个故事,轻松实现从技术到管理的华丽转身!

博文视点Broadview

Java全面学习视频书籍,阿里架构师看到都觉得好,Java自学教程

Java 程序员 后端

HTTP系列之:HTTP缓存

程序那些事

缓存 Netty HTTP 程序那些事

【LeetCode】二叉搜索树的后序遍历序列Java题解

Albert

算法 LeetCode 9月日更

架构实战营模块七作业

老猎人

架构实战营

LeetCode刷题13-简单-罗马数字转整数

ベ布小禅

9月日更

粗放生长时代结束,高精地图收紧灰色地带

脑极体

架构实战训练营|作业|模块2

Frode

#架构实战营

Groovy无痛AOP之旅_Java_John McClean_InfoQ精选文章