Scala: Implementing Java's AspectJ around advice or Python decorators
I have been using Java + AspectJ extensively for my startup. I would love to switch to Scala but I have a common design pattern that I am not sure entirely the best way to implement in Scala.
A tremendous amount of our application uses AspectJ pointcuts using annotations as the marker. This is very similar to Python's decorator and blogged about it here.
I have tried doing this technique in Scala but had problems with AspectJ +开发者_开发知识库 Scala. Even if I did get it to work it seems unScala like.
I have seen some projects do some call-by-name closure magic (I think thats what they are doing).
Example of a replace of @Transaction:
transaction {
// code in here.
}
I have to say though I prefer the annotation more as it seems more declarative. What is the Scala way of declaratively "decorating" code blocks?
Incidentally, I am giving a talk at Scala Days 2011 on the same topic. The core idea is the same as Kim's and Dean's example. However, when it comes to the full spectrum of crosscutting concerns, similarity and differences become more nuanced.
On one end of the spectrum, there are not-really-crosscutting concerns such as caching. When the host language doesn't support higher-order functions (for example, Java), implementing the concern as an aspect becomes attractive. For example, with AspectJ and annotation approach, you can say:
@Cacheable(keyScript="#account.id")
public double getNetWorth(Account account) {
... expensive computation
}
But with higher-order function in Scala, you can do:
def getNetWorth(account: Account) : Double = {
cacheable(keyScript=account.id) {
... expensive computation
}
}
The Scala approach is much better, because:
- Caching is unlikely to be widely applicable. For example, it is unlikely that all method in a class or all public methods in all classes in a package are cacheable. And even if there is such situation, the
keyScript
is unlikely to be the same or easily expressible in a generic form. - The AspectJ approach uses annotation as a crutch to offer a decent implementation. With higher-order function in Scala, the intention is expressed directly.
- The AspectJ approach needs to use an external language (such as OGNL or Spring Expression Language) to compute the key. With Scala, you can just use the host language.
In the middle, there are common crosscutting concerns transaction management and security. On the surface, they look much like caching. However, we find in practice that applying these functionalities to all methods of a class (with generic sub-selection such as those with public access) or all methods of all classes marked with an annotation (say @Service
) is common. If such is the case, the AspectJ approach ends up being superior, since it provides a way to apply the functionality at a higher level that higher-order functions. You no longer have to surround each and every method with transactional {}
or secured {}
, when a class-level annotation will do just fine. For security-like concerns, AspectJ approach allows an easier way to perform security auditing.
On the other end of the spectrum are crosscutting concerns such as tracing, profiling, monitoring, policy enforcement, auditing, some forms of concurrency control (such as Swing's/SWT/Android UI thread dispatching), etc. These lend themselves very well to be selected by a pointcut (with and often without annotations). It is very difficult to do the same in a consistent manner using higher-order functions alone.
There are more semantic nuances, but the bottom line is that when you find annotating every method to have a crosscutting concern applied, higher-order function is likely to be a better approach. For others, using Scala with AspectJ is likely to provide consistent and compact solution.
p.s. I haven't tried AspectJ+Scala in Eclipse lately (since Scala in Eclipse started to work only recently). But external build using Maven worked fine after http://lampsvn.epfl.ch/trac/scala/ticket/4214 was fixed.
The scala way would be
def transaction(f: =>Unit) = {
println("start transaction")
f
println("end transaction")
}
transaction {
println("inside transaction")
}
This prints
start transaction
inside transaction
end transaction
There are other advantages with using the transaction method approach vs. annotations. You can add catch and finally clauses to ensure proper resource cleanup. To extend Kim's example a bit:
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")
}
You can also make calls to transaction inside method bodies, whereas you can't annotate a block internal to a method. Of course, you could just make that internal block another method call with an annotation.
I understand the attraction of annotations, and XML configuration files for that matter, from my Java days, but these days, I prefer having everything written as "normal" code, because of the uniformity and greater expressive power. I only use annotations when I'm calling a Java library that requires them. Also, if you make your code as "functional" as possible, then everything is declarative! ;)
精彩评论