Scala:实现Java的AspectJ环绕通知或Python装饰器
我一直在我的创业公司里广泛使用Java和AspectJ。我很想转向Scala,但有一个常见的设计模式,我不太确定在Scala中最好的实现方式是什么。
我们应用程序中有大量的代码使用AspectJ的切入点,主要是通过注解来标记。这和Python中的装饰器非常相似,这里有相关的博客。
我尝试在Scala中使用这种技术,但在AspectJ和Scala的结合上遇到了问题。即使我能让它工作,感觉也不太像Scala。
我见过一些项目使用按名称调用的闭包魔法(我想他们就是这样做的)。
这是一个替代@Transactional的例子:
transaction {
// code in here.
}
不过我得说,我更喜欢使用注解,因为它看起来更具声明性。在Scala中,如何以声明的方式“装饰”代码块呢?
3 个回答
使用事务方法的好处不仅仅是注解。你可以添加捕获(catch)和最终(finally)语句,确保资源能够正确清理。为了更好地理解金的例子,我们可以稍微扩展一下:
def transaction(f: =>Unit) = {
println("start transaction")
try {
f
println("end successful transaction")
} catch {
case ex =>
// rollback?
println("end failed transaction")
} finally {
// cleanup?
println("end cleanup")
}
}
transaction {
println("inside transaction")
}
你还可以在方法内部调用事务,而不能在方法内部的代码块上使用注解。当然,你也可以把这个内部代码块单独写成一个方法,并给它加上注解。
我理解注解和XML配置文件的吸引力,尤其是在我使用Java的时候,但现在我更喜欢把所有东西都写成“普通”的代码,因为这样更统一,表达能力也更强。只有在调用需要注解的Java库时,我才会使用注解。此外,如果你尽量让代码“函数式”,那么一切都是声明式的!;)
Scala的写法是这样的
def transaction(f: =>Unit) = {
println("start transaction")
f
println("end transaction")
}
transaction {
println("inside transaction")
}
这段代码会输出
start transaction
inside transaction
end transaction
顺便提一下,我将在2011年Scala大会上讲同样的话题。核心思想和Kim和Dean的例子是一样的。不过,当涉及到各种交叉关注点时,类似和不同之处就变得更复杂了。
在一个极端,有一些并不是真正的交叉关注点,比如缓存。当主语言不支持高阶函数(比如Java)时,把这个关注点实现为一个切面就显得很有吸引力。例如,使用AspectJ和注解的方法,你可以这样写:
@Cacheable(keyScript="#account.id")
public double getNetWorth(Account account) {
... expensive computation
}
但是在Scala中使用高阶函数,你可以这样做:
def getNetWorth(account: Account) : Double = {
cacheable(keyScript=account.id) {
... expensive computation
}
}
Scala的方法要好得多,因为:
- 缓存不太可能广泛适用。例如,一个类中的所有方法或者一个包中所有类的公共方法都不太可能都能被缓存。即使有这样的情况,
keyScript
也不太可能是相同的,或者很容易用通用的形式表达。 - AspectJ的方法使用注解作为一种辅助工具来提供一个不错的实现。而在Scala中,高阶函数直接表达了意图。
- AspectJ的方法需要使用外部语言(比如OGNL或Spring表达式语言)来计算键。而在Scala中,你可以直接使用主语言。
在中间,有一些常见的交叉关注点,比如事务管理和安全性。从表面上看,它们和缓存很相似。不过,实际上我们发现将这些功能应用于一个类的所有方法(比如那些具有公共访问权限的方法)或者所有标记了注解(比如@Service
)的类的方法是很常见的。如果是这样的话,AspectJ的方法就更优越,因为它提供了一种在更高层次上应用功能的方法,而高阶函数则不行。你不再需要在每个方法周围加上transactional {}
或secured {}
,一个类级别的注解就足够了。对于安全相关的关注点,AspectJ的方法也提供了更简单的安全审计方式。
在另一个极端,有一些交叉关注点,比如跟踪、性能分析、监控、政策执行、审计,以及某些形式的并发控制(比如Swing/SWT/Android的UI线程调度)等。这些很适合通过切点来选择(有时带有注解,有时不带)。仅仅使用高阶函数来做到这一点是非常困难的。
还有更多的语义细微差别,但总的来说,当你发现需要给每个方法加注解来应用交叉关注点时,高阶函数可能是更好的方法。对于其他情况,使用Scala和AspectJ可能会提供一致且简洁的解决方案。
附言:我最近没有在Eclipse中尝试AspectJ+Scala(因为Scala在Eclipse中最近才开始工作)。不过在修复了http://lampsvn.epfl.ch/trac/scala/ticket/4214之后,使用Maven的外部构建运行得很好。