Friday, September 24, 2010

Interceptor Advisor Pattern

Your standard CDI/EJB Interceptor example uses a logger as an around advice. Generally this gives you an interceptor that looks like so:

@Log
public class LoggingInterceptor {

    private java.util.logging.Logger logger =
            java.util.logging.Logger.getLogger("theLogger");

    @AroundInvoke
    public Object intercept(InvocationContext context) throws Exception {
        logger.info("" + context.getMethod().getName());
        return context.proceed();
    }
}

As of the Interceptors 1.1 spec, you can bind that to a bean via creating our own javax.interceptor.InterceptorBinding annotation. In our example, we've created one called @Log:

@InterceptorBinding
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

Now we just apply that to the bean as follows.

@Log
public class FooBean {

    public void somethingCommon(){
        //...
    }

    public void somethingImportant() {
        //...
    }

    public void somethingNoteworthy() {
        //...
    }
}

Great! Now we are done. Every time that bean is invoked, the LoggerInterceptor will issue a log message on info level. Aren't interceptors wonderful! End of story, right? Not quite.

Fundamentally, our example is still very contrived. Who wants to log everything on the same level?

Here is a little pattern that you can use to better advise your LoggerInterceptor around advice. First, we create a couple new annotations for log levels: @Fine and @Info

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Fine {
}

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Info {
}

Then we apply those to the bean...

@Log
public class FooBean {

    public void somethingCommon(){
        //...
    }

    @Info
    public void somethingImportant() {
        //...
    }

    @Fine
    public void somethingNoteworthy() {
        //...
    }
}

Now we alter our LoggerInterceptor to check for our new annotations and alter its behavior.

@Log
public class LoggingInterceptor {

    private java.util.logging.Logger logger =
            java.util.logging.Logger.getLogger("theLogger");

    @AroundInvoke
    public Object intercept(InvocationContext context) throws Exception {
        final Method method = context.getMethod();
        if (method.isAnnotationPresent(Info.class)) {
            return info(context);
        } else if (method.isAnnotationPresent(Fine.class)) {
            return fine(context);
        } else {
            return finest(context);
        }
    }

    public Object info(InvocationContext context) throws Exception {
        logger.info("" + context.getMethod().getName());
        return context.proceed();
    }

    public Object fine(InvocationContext context) throws Exception {
        logger.finest("" + context.getMethod().getName());
        return context.proceed();
    }

    public Object finest(InvocationContext context) throws Exception {
        logger.finest("" + context.getMethod().getName());
        return context.proceed();
    }
}

Done! Now we have a pattern to advise our interceptor!

This will totally work today. But this pattern is so simple and elegant what if we could support it right inside the container? Imagine how cool it would be if we could just do this in our interceptor and the container would just figure it out.

@Log
public class LoggingInterceptor {

    private java.util.logging.Logger logger =
            java.util.logging.Logger.getLogger("theLogger");

    @Info
    public Object info(InvocationContext context) throws Exception {
        logger.info("" + context.getMethod().getName());
        return context.proceed();
    }

    @Fine
    public Object fine(InvocationContext context) throws Exception {
        logger.finest("" + context.getMethod().getName());
        return context.proceed();
    }

    @AroundInvoke
    public Object finest(InvocationContext context) throws Exception {
        logger.finest("" + context.getMethod().getName());
        return context.proceed();
    }
}

Definitely something I plan to propose for the next round of specifications....

As always, comments welcome.