Sunday, April 20, 2008

Tapestry AOP: MethodAdvice

I don't go out of my way to talk about Tapestry 5 IoC, since it is really just a side issue compared to the main framework (but it is the infrastructure that allows Tapestry to be everything it is).

However, the IoC container is quite powerful and fully featured and quite useful on its own. However, one feature of the container wasn't quite up to snuff: service decorators. Service decorators are a limited form of aspect oriented programming wherein its possible to "wrap" a service with an interceptor that provides additional behavior. Tapestry includes two decorators: one for adding logging of method invocations, results and exceptions and a second decorator for managing Hibernate transactions.

Tapestry is responsible for invoking a decorator method, but the responsibility of that method was to return an interceptor object that encapsulates the behavior. So, a logging interceptor is responsible for logging method entry and exits around re-invoking each method on the service, and a transaction interceptor is responsible for commiting or aborting the transaction after invoking the service method.

The problem is how to provide such an interceptor. Option A is to use a JDK dynamic proxy. This is a perfectly good solution, but has a few annoyances; you drop out of pure bytecode which means that Hotspot will not be able to fully optimize your code. In addition, there a minute cost associated with constantly boxing and unboxing primitives and packaging parameters up as an object array.

Option B is to use Tapestry's ClassFactory and ClassFab interfaces to create a new class and then instantiate. Javassist is under the surface, but the APIs are tamed. Even so, its a lot of work and a lot to test to check all the edge cases such as handling of primitives (more boxing and unboxing). This was the approach I've been taking in the past.

So I've taken some inspiration from the AOP Alliance approach.

You can now create an interceptor using a special service, AspectDecorator, and providing MethodAdvice. For example, the HibernateTransactionDecorator (which looks for the @CommitAfter annotation) is simplified to:

public class HibernateTransactionDecoratorImpl implements HibernateTransactionDecorator
{
    private final AspectDecorator _aspectDecorator;

    private final HibernateSessionManager _manager;

    private final MethodAdvice _advice = new MethodAdvice()
    {
        public void advise(Invocation invocation)
        {
            try
            {
                invocation.proceed();
            }
            catch (RuntimeException ex)
            {
                _manager.abort();

                throw ex;
            }

            // For success or checked exception, commit the transaction.

            _manager.commit();
        }
    };

    public HibernateTransactionDecoratorImpl(AspectDecorator aspectDecorator, HibernateSessionManager manager)
    {
        _aspectDecorator = aspectDecorator;

        _manager = manager;
    }

    public <T> T build(Class<T> serviceInterface, T delegate, String serviceId)
    {
        String description = String.format("<Hibernate Transaction interceptor for %s(%s)>",
                                           serviceId,
                                           serviceInterface.getName());

        AspectInterceptorBuilder<T> builder = _aspectDecorator.createBuilder(serviceInterface, delegate, description);

        for (Method m : serviceInterface.getMethods())
        {
            if (m.getAnnotation(CommitAfter.class) != null)
            {
                builder.adviseMethod(m, _advice);
            }
        }

        return builder.build();
    }
}

At this weekend's No Fluff Just Stuff, Neal Ford went out of his way to point out how hard meta-programming in Java is, and how impressed he was by what I've accomplished using meta-programming throughout Tapestry. Even so, he and the other speakers voted me "most in need of a dynamic programming language" behind my back!

Meanwhile, this MethodAdvice approach (the terminology is borrowed from AOP and AspectJ) really is a very succinct way of expressing this transaction concern, and it's so easy to mix and match methods within the interface, advising some of them and letting the others just "pass thru". There's going to be a lot of room to use this same technique to provide many other cross-cutting concerns such as performance monitoring, security checks ... even checking for null pointers!

6 comments:

  1. Great staff Howard! It was indeed one of the pain points of T5 IOC compared with Hivemind. AOP Alliance support is very good decision IMHO.

    ReplyDelete
  2. I'm afraid its "inspired by" rather than "utilizing". Partly to avoid Yet Another Dependency, partly because I only needed one small piece, partly because I wanted the specific MethodAdvice and Invocation interfaces to be just exactly what T5 IoC needs. But I think you could "wrap" the Tapestry MethodAdvice and make something that looks a lot more like a AOP Alliance interface.

    ReplyDelete
  3. so you call this stuff a "side issue" ... that's not exactly what i have in mind.

    The IoC is going to serve a lot of development in my environment, i'm eager to see if/when it comes to a somewhat standalone project...

    ReplyDelete
  4. I agree with Massimo. I'm using chainbuilders to handle the ordering a rendering logic of tabs on our main pages. I'm using a masterdispatcher to handle our logins (and may use it for our OpenID implementation). I'm using object rendering strategies for some displays.

    These are central parts of our app.

    Formos could build a great business opportunity around training on the IOC and the meta-programming required.

    Port to smalltalk for dynamicism :)

    ReplyDelete
  5. The IoC framework is completely standalone; you can absolutely use it outside of Tapestry; you can use it for non-web applications. If I was building an ESB of some flavor, I would almost certainly start cranking out logic using tapestry-ioc; perhaps builders that make Queues and Topics looks like services, etc.

    On the Tapestry side, I'm hoping to put together a similar system whereby component methods can be advised, similar to how it works for services. This would be used by ComponentClassTransformWorker instances. There would be a ComponentMethodAdvise and ComponentInvocation interfaces, the same as the above, except that the component's ComponentResources object would be made available to the advise.

    ReplyDelete
  6. Anonymous10:38 PM

    Hi!

    this is great stuff! I have very conservative three tier system running. (Data access layer, business logic layer and ui.) All tiers are divided to interfaces and implementation and are arranged as individual projects. Currently Spring ties all this together, but as it is the only thing it does currently (transaction management also) i was thinking wether I should use T5 ioc for this. Is there an example of a "Heavy weight" project that uses hibernate and previously mentioned project structure? Tapestry-hibernate is not the answer as we don't want tapestry (web portion) to have any clue how data access layer persists the entities, or what technology is used. Session per request is currently used.

    ReplyDelete

Please note that this is not a support forum for Tapestry. Requests for help will be deleted. Please subscribe to the Tapestry user mailing list if you are in need of support, or contact me directly for professional (for pay) support.

Spammers: Don't bother. I delete your comments and it's a waste of time for both of us. 垃圾邮件发送者:不要打扰。我删除您的评论和它的时间对我们双方的浪费