MacWire 0.5: Interceptors

16 Flares 16 Flares ×

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:

1
2
3
4
5
6
7
8
9
10
11
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 invocation
  • NoOpInterceptor – 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

  • http://ceylon-lang.org/blog/authors/gavin-king/ Gavin King

    > order of interceptor invocation is simply ordering of declarations

    This isn’t the right way to do it. There are, in general, quite hairy dependencies between interceptors, and you shouldn’t leave it up to the developer of the intercepted class to know what order the interceptors have to be called in. (This is an aspect of DRY.)

    Of course, there’s an even bigger problem of this nature with Scala’s inheritance model, which features stateful traits and depth-first linearization, and thus the potential for ordering-related bugs. (Any sort of depth-first linearization is an obvious language-design smell.)

  • http://www.warski.org/ Adam Warski

    Re: ordering of interceptors: true, having to repeat the “right” order isn’t DRY, and centralization is needed here. However, this could be solved e.g. by having an InterceptorOrdering class which would take a list of interceptors, and apply then in the correct order:


    lazy val someService = ordered(transactional, security)(new SomeService)
    def ordered: InterceptorOrdering
    def transactional: Interceptor
    def security: Interceptor

    This would also leave you the possibility to apply them in a specific order if there’s a need (hence we are not constrained by a container).

    Re: trait ordering: true that stateful trait ordering sometimes is a problem, but that’s usually solved by using def-s or lazy val-s. Or did you mean something else? Can’t speak for depth-first linearization, as I never researched alternatives, so I don’t feel qualified to answer.

  • http://ceylon-lang.org/blog/authors/gavin-king/ Gavin King

    All I’m saying is the whole point of having these kinds of constructs it to permit easy composition. Therefore, that they should compose together in a non-fragile way is a primary design goal.

  • http://www.warski.org/ Adam Warski

    Agreed; and I think that by using language constructs, instead of some container-defined ones (be it annotations or xml), you can achieve better composability; although this may sometimes mean using “patterns” in the language, that is using language features in some specific way (e.g. not using val-s but lazy val-s when doing “manual DI”)

    In some way it’s the non-verbalized goal of this project: to show that you very often don’t need a “container” to do DI, scopes or interceptors etc., and that these things are often much simpler than we might think.

  • A. Karimi

    What about life time management? How can I dispose a disposable object using MacWire?

  • http://www.warski.org/ Adam Warski

    Not sure what you mean :) All objects in MacWire are completely normal Scala objects, which means they are subject to GC when they stop being referenced.

  • A. Karimi

    Sometimes we have to deal with native resources which we want to release them ASAP (even DB connections). Of course I have a .NET background and we had IDisposable there. Maybe I don’t have to deal with it in JVM world?! Right?

  • http://www.warski.org/ Adam Warski

    AutoCloseable can be used e.g. with Java’s try-with-resources. In Scala it’s fairly easy to implement such construct without native language support.

    As for closing everything after a HTTP request is finished, there’s nothing out-of-the-box, but it wouldn’t be hard to code. Though it’s probably best to close the resources immediately after they are no longer needed.

  • A. Karimi

    It’s about testing; If I create a new instance of such object and close it manually then unit-testing wouldn’t be easy. Is there any other solution for these situations?

  • http://www.warski.org/ Adam Warski

    You can solve such problems in many elegant, general ways, but it’s not something that Scala as a language is concerned about, at it’s also not the problem that MacWire tries to solve.

16 Flares Twitter 3 Facebook 4 Google+ 8 LinkedIn 1 Email -- 16 Flares ×