BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Groovy无痛AOP之旅

| 作者 John McClean 关注 0 他的粉丝 ,译者 胡键 关注 0 他的粉丝 发布于 2007年10月15日. 估计阅读时间: 20 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

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"
def c = { parameter | println(printMe + parameter} }
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 :-


Calling print message with AOP before advice to set message :-
hello world


Calling print message with AOP after advice to output status :-
hello world
Status:message displayed


Calling print message via invokeMethod on a Statically defined Object :-
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:-
Saved model 10 successfully


Attempting save with AOP security & incorrect id:-
Sorry you do not have permission to save this model (10)


Attempting save with AOP security & correct id:-
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:-
save operation
Saved model 10 successfully


Attempting save with AOP security & incorrect id:-
Sorry you do not have permission to save this model (10)


Attempting save with AOP security & correct id:-
save operation
Saved model 10 successfully


Attempting save with save method removed (perhaps for security reasons):-
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语言强大的能力。

资源

  • 获得本文中使用的代码
  • 获得Groovy
  • Groovy Eclipse插件
  • 一篇Java中的AOP介绍
  • 一篇Groovy的介绍 (非常老)
  • Groovy中的闭包
  • 更多关于Groovy的MOP
  • 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

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

闭包参数语法? by Guo Ford

groovy的闭包参数好像还是就的语法?

Re: 闭包参数语法? by 胡 键

这篇文章也比较老了,呵呵。

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT