Interceptors are very useful for implementing cross-cutting concerns. Classic use-cases include security, logging or transaction support. Since version 0.5, MacWire contains an implementation of interceptors which can be applied to arbitrary object instances in a Scala-friendly way, and which plays nicely with the traits-as-modules approach. No compile-time or load-time bytecode manipulation is required; only javassist is used at run-time to generate a proxy.
MacWire defines an Interceptor
trait, which has just one method: apply
. When applied to an object, it should return an intercepted instance.
Suppose we have a couple of objects, and we want each method in these objects to be surrounded by a transaction. The objects are defined and wired inside a trait (could also be inside a class/object). That’s of course a perfect fit for an interceptor; we’ll call the interceptor transactional
. We will also make it abstract so that we can swap implementations for testing, and keep the interceptor usage declarative:
trait BusinessLogicModule {
// not intercepted
lazy val balanceChecker = new BalanceChecker()
// we declare that the usage of these objects is transactional
lazy val moneyTransferer = transactional(new MoneyTransferer())
lazy val creditCard = transactional(new CreditCard())
// abstract interceptor
def transactional: Interceptor
}
MacWire provides two interceptor implementations:
ProxyingInterceptor
– proxies the given instance, and returns the proxy. A provided function is called on invocationNoOpInterceptor
– useful for testing, when applied returns the instance unchanged
A proxying interceptor can be created in two ways: either by extending the ProxyingInterceptor
trait, or by passing a function to the ProxyingInterceptor
object. For example:
object MyApplication extends BusinessLogicModule {
lazy val tm = new TransactionManager()
// Implementing the abstract interceptor
lazy val transactional = ProxyingInterceptor { ctx =>
// This function will be called when a method on the intercepted
// object is invoked
try {
// Using objects (dependencies) defined in the application
tm.begin()
// Proceeding with the invocation: calls the original method
val result = ctx.proceed()
tm.commit()
result
} catch {
case e: Exception => {
tm.rollback()
throw e
}
}
}
}
The ctx
instance contains information on the invocation, such as the method being called, the parameters or the target object. Another example of an interceptor, which uses this information, is a TimingInterceptor
, defined in the trait-extension style:
object TimingInterceptor extends ProxyingInterceptor {
def handle(ctx: InvocationContext) = {
val classWithMethodName = s"${ctx.target.getClass.getSimpleName}.${ctx.method.getName}"
val start = System.currentTimeMillis()
println(s"Invoking $classWithMethodName...")
try {
ctx.proceed()
} finally {
val end = System.currentTimeMillis()
println(s"Invocation of $classWithMethodName took: ${end-start}ms")
}
}
}
You can see this interceptor in action in the MacWire+Scalatra example, which also uses scopes and the wire[]
macro. Just explore the code on GitHub, or run it by executing sbt examples-scalatra/run
after cloning MacWire and going to http://localhost:8080.
Interceptors can be stacked (order of interceptor invocation is simply ordering of declarations – no XML! :) ), and combined with scopes.
Note that although the code above does not use wire[]
for instance wiring, interceptors of course work in cooperation with the wire[]
macro. The interceptors can be also used stand-alone, without even depending on the macwire-macros
artifact.
The interceptors in MacWire correspond to Java annotation-based interceptors known from CDI or Guice. For more general AOP, e.g. if you want to apply an interceptor to all methods matching a given pointcut expression, you should use AspectJ or an equivalent library.
If you’d like to try MacWire, just head to the GitHub project page, which contains all the details on installation and usage.
What do you think about such an approach to interceptors?
Adam
comments powered by Disqus