Friday, April 25, 2008

Tapestry: Components + Aspects

When I was first starting on the Tapestry 5 code base, I started using AspectJ for parts of the framework. I wrote some clever aspects for defensive programming (cementing "don't accept null, don't return null" as an iron-clad rule) and created some aspects to simplify concurrent access to certain resources.

I certainly never expected Tapestry developers to have to use AspectJ: it's certainly a powerful tool, but it has downsides: AspectJ affects the build process in a signifcant way, it's tricky to use properly, it adds a huge runtime dependency and the plugin for Eclipse made the IDE slow(-er) and (more) unstable. I eventually stripped out the AspectJ code entirely and restricted the framework to traditional code (plus, of course, Javassist).

In the interrum a number of aspect oriented touches have appeared in Tapestry. Certainly, the entire mixins concept is an approach to aspect orientation. Likewise, service decorators. In fact, much of the class transformation mechanism is a way of performing aspect-oriented operations on Tapestry component classes. Creating your own parameter binding prefixes (something I often do for client projects) is another aspect: one focused on streamlining how data gets into or out of a component.

However, all of these techniques can be cumbersome; they are built on the Javassist "language" which is another tool that can be difficult to use properly. I've created layers around Javassist to make things easier, but it's still an uphill battle.

Earlier in the week I was able to take the Javassist out of service method advice. However, that's not quite enough ... I want it to be easier to apply advice to component methods just as easily.

Well, I did just that. There is a ComponentMethodAdvice that can be applied to component methods. It's only a slight variation from the MethodAdvice used on service interfaces (the difference being that the invocation allows access to the ComponentResources of the component).

Here's a concrete example: Part of tapestry-hibernate is the @CommitAfter annotation, which is used to commit the running transaction after invoking a method. There's a decorator that can be applied to services, and there's a worker that is automatically threaded into the Component Class Transformation chain of command.

The worker used to use the Javassist approach to decorate method invocations with the desired logic: commit the transaction when the method completes normally, or with a checked exception. Abort the transaction when the method throws a runtime exception.

First, the old code:

public class CommitAfterWorker implements ComponentClassTransformWorker
{
    private final HibernateSessionManager _manager;

    public CommitAfterWorker(HibernateSessionManager manager)
    {
        _manager = manager;
    }

    public void transform(ClassTransformation transformation, MutableComponentModel model)
    {
        for (TransformMethodSignature sig : transformation.findMethodsWithAnnotation(CommitAfter.class))
        {
            addCommitAbortLogic(sig, transformation);
        }
    }

    private void addCommitAbortLogic(TransformMethodSignature method, ClassTransformation transformation)
    {
        String managerField = transformation.addInjectedField(HibernateSessionManager.class, "manager", _manager);

        // Handle the normal case, a succesful method invocation.

        transformation.extendExistingMethod(method, String.format("%s.commit();", managerField));

        // Now, abort on any RuntimeException

        BodyBuilder builder = new BodyBuilder().begin().addln("%s.abort();", managerField);

        builder.addln("throw $e;").end();

        transformation.addCatch(method, RuntimeException.class.getName(), builder.toString());

        // Now, a commit for each thrown exception

        builder.clear();
        builder.begin().addln("%s.commit();", managerField).addln("throw $e;").end();

        String body = builder.toString();

        for (String name : method.getExceptionTypes())
        {
            transformation.addCatch(method, name, body);
        }
    }
}

Can you tell where that logic (when to commit and abort) is? Didn't think so, it's obscured by lots of nuts-and-bolt logic for creating the Javassist method body.

All that stuff with the BodyBuilder is a way to assemble the Javascript method body piecewise. It's certainly meta-programming, but it's only a few steps ahead of writing the Java code to a temporary file and compiling it. That's great for people into the meta-coding concept (count me in) but sets the bar so high that most people will never be able to tap into the power.

That's where the advice comes in: the new version of the worker is much simpler:

public class CommitAfterWorker implements ComponentClassTransformWorker
{
    private final HibernateSessionManager _manager;

    private final ComponentMethodAdvice _advice = new ComponentMethodAdvice()
    {
        public void advise(ComponentMethodInvocation invocation)
        {
            try
            {
                invocation.proceed();

                // Success or checked exception:

                _manager.commit();
            }
            catch (RuntimeException ex)
            {
                _manager.abort();

                throw ex;
            }
        }
    };

    public CommitAfterWorker(HibernateSessionManager manager)
    {
        _manager = manager;
    }

    public void transform(ClassTransformation transformation, MutableComponentModel model)
    {
        for (TransformMethodSignature sig : transformation.findMethodsWithAnnotation(CommitAfter.class))
        {
            transformation.advise(sig, _advice);
        }
    }
}

Really simple! The advice is very clear on what happens when, as long as you realize that proceed() invokes the original method (the method "being advised"), and also Tapestry's way of handling thrown exceptions: runtime exceptions are not caught by proceed(), but checked exceptions are.

This simplicity opens up the power of aspects without all mental overhead of AspectJ's join points and declarative point-cut language. It's only a fraction of AspectJ's power, but it's a very useful fraction.

Further, AspectJ works entirely in a static way (it primarily works at build time), but this style of AOP is a mix of declarative and imperative. In other words, we can have active code decide on a component-by-component and method-by-method basis what methods should be advised. The above example is typical: it's driven by an annotation on the method ... but other cases jump to mind: triggered by naming conventions, by parameter or return types, or by thrown exceptions. And there's no guesswork about going from the advice to Tapestry services: its the same uniform style of injection used throughout Tapestry.

In the future, we may add other options, such as advice for field access, but what's just checked in is already a huge start.

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!

Thursday, April 17, 2008

Service Dependencies in Tapestry 5

I was curious to see if I could generate a diagram that showed how all the services inside Tapestry 5 are interconnected. In the diagram, solid lines are dependencies, dashed lines are contributions, and dotted lines represent a services that listens to events from another service.

The final diagram is a bit complex. Ok, that's a tremendous understatement. Yellow nodes are public services, grey nodes are internal services, and orange nodes are simple beans (contributed into services) ... those beans, though, may still have their own dependencies. I think OmniGraffle choked on the complexity (why do some of the lines zip around so aimlessly?) and kind of "phoned it in" in terms of layout. I had hoped for something that could go on a T-shirt.

Worse yet, this is only a partial diagram; I didn't cover many of the dependencies that arrive as contributions to services (there's just a few of those in there). Those contributions would show up as even more orange nodes; these additional nodes would provide connections to many of the outliers that appear to be unreferenced.

Is this good or bad? Is it too complex? Can it be simplified? Does all that really need to be in there?

I think it is good: When I'm coding, I find it easy enough to work from a service interface, to the implementation of the interface, to the dependencies of the implementation. This works for me when developing Tapestry itself, or writing apps in Tapestry. Since there are so many small objects, no single object is large, or complex, or hard to understand or debug. It's a bit daunting to know where to start, I'm sure. However, only a few of these services are all that relevant for day-to-day development of Tapestry.

The moral of the story is that simplicity is hard! In Tapestry, pages and templates are as simple and straight forward as I think is possible. There's no excess, no distractions, no clutter, no boilerplate. Much of the logic represented in the diagram is to "fill around the edges" of this simplicity, to bridge the gap from the simple Tapestry component model to the much more complex world of stateless HTTP and multi-threaded request handling.

New Tapestry 5 Features

I've been using Tapestry 5 for a client project, which is a great chance to find the rough edges. If you've been following the bug list, you'll have seen lots of minor bug fixes, many related to JavaScript.

I've also added a couple of powerful new features.

The Grid component now has an inplace parameter; setting this to true "Ajax-ifies" the Grid; it updates in place. Clicking any of the paging or sorting controls will repaint the Grid in place, without affecting the rest of the page. Handy. I had originally though in terms of creating an AjaxGrid component for this purpose capable of redrawing individual cells ... now I'm not so sure that's necessary.

Perhaps more importantly, I've added a new annotation, @CommitAfter, that can be placed on component methods, to indicate the Hibernate transaction should be committed after invoking the method. It can also be used with services, via a decorator. In any case, much of the work was contributed by Igor Drobiazko.

These features are available in the nightly build, and will be part of Tapestry 5.0.12.

They are also a good indication of how Tapestry future backwards compatibility will work. The new behaviors "slots in" with the existing behavior. In this case, a new annotation triggers new behavior, or a new parameter is added to an existing component. These are the vectors by which Tapestry adapts to your code (and not vice-versa).

In the future, we'll be adding much more dramatic new features, as well as integrations with other frameworks, such as Seam, Spring Web Flow, Lucene, Quartz and other parts of the evolving standard open source stack. But due to the baked in elegance of Tapestry, they will integrate with barely a ripple.

Tuesday, April 15, 2008

A round-up of Tapestry blogs

I was just fiddling with Google's Blog Search and searched for "java tapestry"; here's a few things I stumbled upon:

Fun stuff ... and I have more advancements for Tapestry 5 about ready to announce!