Thursday 27 January 2011

Extending Bean Functionality using Interceptors

A key part of the Java EE 6 platform is CDI (Context and Dependency Injection) and CDI supports two ways of extending bean functionality: interceptors and decorators.

For this post, we'll concentrate on interceptors and provide an example of implementing a cross-cutting concern across multiple beans ie logging.

An example of an interceptor which extracts the user value from the SessionContext for logging is shown below:

import org.apache.log4j.MDC;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import java.io.Serializable;

public final class AuditLoggingInterceptor implements Serializable {

    protected static final String MDC_USER = "user";
    protected static final String UNAUTHENTICATED_USER = 

        "Unauthenticated User";
    protected static final String NULL_USER = 

        "Authenticated User Name is Null";
    private static final long serialVersionUID = 1L;
    private SessionContext sessionContext;

    @AroundInvoke
    public Object captureUser(InvocationContext context) 

        throws Exception {

        MDC.put(MDC_USER, determineUsername());

        try {
            return context.proceed();
        }catch(Exception e){
            // handle exception
        }finally {
            MDC.remove(MDC_USER);
        }
    }

    /**
     * Inject the calling Session Context.
     * @param sessionContext context the current invocation
     */
    @Resource
    public void setSessionContext(SessionContext sessionContext) {
        this.sessionContext = sessionContext;
    }

    public SessionContext getSessionContext() {
        return sessionContext;
    }

    /**
     * Determine what the username should be based on the session context.
     *
     * @return the username
     */
    protected String determineUsername() {
        String username = NULL_USER;
        if (sessionContext != null) {
            try{
                if (sessionContext.getCallerPrincipal() == null) {
                    username = UNAUTHENTICATED_USER;
                } else {
                    username = 

                    sessionContext.getCallerPrincipal().getName();
                }
            }catch(NullPointerException npe){
                // handle exception
            }
        }
        return username;
    }
}


The above interceptor implements a captureUser method which is marked with the @AroundInvoke annotation. This ensures that the method is invoked around the business methods for the classes that  the interceptor is bound to.



import org.apache.log4j.Logger;
import javax.interceptor.Interceptors;
import javax.ejb.Remote;
import javax.ejb.Stateless;

@Interceptors({AuditLoggingInterceptor.class})
@Stateless(mappedName = JndiResourceName.BUSINESS_SERVICE)
@Remote(BusinessService.class)
public class BusinessServiceBean implements BusinessService {

    private static final Logger LOGGER =

        Logger.getLogger(BusinessServiceBean .class);
 
    @Override
    public void doSomething() {
        LOGGER.info("BusinessServiceBean.doSomething ()");
        // do something
    }
}

When the doSomething method is invoked on the business service, the method will be wrapped by captureUser method. The log4j class MDC (Mapped Diagnostic Context) will set the user key in the context map to be the caller principal name from the session context. This value will be used when the info  message is logged. When the method completes, the user value is removed from the context.

The entry in the log4j.properties file so that the MDC value gets picked up is below:

log4j.appender.AUDITROLLINGFILE.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss}  [%X{user}] %c{1} [%p] %m%n

To exclude a class's methods from being 'intercepted' then the business methods can be annotated with @ExcludeClassInterceptors.