Monday, November 12, 2007

Stripes Interceptor Tutorial

Note: I still can't get code to display very well on blogger. Sorry for the formatting issues. If you need to the source code is linked at the bottom of this article if it makes it easier to follow along.

I've been using Stripes for both large and small applications for about a year. While I am of the camp that there is no golden hammer I've not run across a project I am involved with where Stripes doesn't make sense. I've been wanting to write more articles on Stripes. While the documentation on the site is great for folks to get started it is lacking in the area of best practices and general tutorials on different aspects of the framework. One of my favaorite things about Stripes (and there are many) is how easy it is to extend the framework to suit my needs and most of the time I don't even need to dig into the source. A lot of tiimes that power comes in the form of an Interceptor.

Interceptors are not a new concept. I believe my first experince with them in web applications was when working with WebWork several years ago. Stripes has a couple of Interceptors already:

  • SpringInterceptor - Allows you to inject Spring Beans into controllers using the @SpringBean method level annotation.
  • BeforeAfterMethodInterceptor - Allows you to execute methods before and/or after specific Lifecycle Stages using @Before and @After method level annotations (AOP like behavior)

Note: Both the Interceptors mentioned above work in combination with annotations but not all have to.

In this article I want to show how to create an interceptor and we'll also create an annotation that will be used with the interceptor. I'll identify a problem that Stripes by itself doesn't solve and show how to implement the solution with a simple Interceptor. At times you may see mention of concepts that you won't be familiar with if not familiar with Stripes. While I'll try and explain these a bit as I go I may link to the Stripes documentation on the subject so as not to repeat information. I also want to keep this article as concise and to the point as possible.

The Problem

Ajax is everywhere and one common problem I've run into in the past is dealing with the browser caching the response from an ajax request. This seems to happen more often than not in Internet Explorer. What we want is to prevent this as much as possible when needed.

Typical Solutions

Probably the most common talked about solution is appending a random number on the end of the request. This tricks the browser into thinking it is a different response. Wikipedia's entry on XmlHttpRequest offers a different and slightly more complex solution which you can read about here: http://en.wikipedia.org/wiki/Xmlhttprequest#Caching. The problem with that is most of us are using a JavaScript library like jQuery or Prototype and that would cause us to go hacking inside their code. Not ideal. And another approache is to add header information to the response. In java it goes something like this:

HttpServletResponse response = context.getResponse();

response.setDateHeader("Expires", 0);

response.setHeader("Cache-control", "no-cache");

response.setHeader("Pragma", "no-cache");

Finding a centralized place for that code is the key. One solution on the Stripes mailing list was having a disableCaching() method in an ActionBean base class. When you wanted this to execute you would do something like this:

@Before(stages = LifecycleStage.ResolutionExecution)

@Override

protected void disableCaching() {

super.disableCaching();

}

And while that works it is a bit more verbose than need be.

The Interceptor Solution

What we want to do is take the above code and centralize it in an Interceptor that executes on the LifecycleStage.ResolutionExecution stage however we want to be able to control when the code gets executed. To do this we need two things:

  1. @NoCache annotation - We'll use this on methods to denote we want no caching. We'll also allow a boolean argument to indicate whether it should be on or off. This is important if we want to override a class level annotation on one or more methods.
  2. NoCacheInterceptor - We'll use this to check for the @NoCache annotation and shove the no cache header directives into the response.

The @NoCache annotation is pretty straightfoward:

@Retention(RetentionPolicy.RUNTIME)

@Target( {ElementType.METHOD, ElementType.TYPE})

@Documented

public @interface NoCache

{

boolean value() default true;

}

Save that in a file called NoCache.java in whatever package you desire.

Next is the interceptor which requires a bit more discussion. A Stripes interceptor implements an interface called Interceptor. Interceptor requires one method be implemented; public Resolution intercept(ExecutionContext ctx) throws Exception. Stripes will call this method on any configured interceptor during the specified LifecycleStage. Whoa, how do we specify the LifecycleStage? Whoa!!! What's with all this LifecycleStage talk?

Segway..Stripes has five Lifecycle Stages. Instead of repeating what is already well documented I'll let you read about them at your lesiure here.

And we're back...So we need to create this Interceptor and tell it when to execute the intercept method. We do that by specifying an annotation at the class level:

@Intercepts(LifecycleStage.ResolutionExecution)

public class NoCacheInterceptor implements Interceptor {

...

}

Now we should define our intercept method:

public Resolution intercept(ExecutionContext ctx) throws Exception {
...
}

Inside this method we need to define several objects we are going to use:

(1) final Configuration config = StripesFilter.getConfiguration();
(2) final ActionResolver resolver = config.getActionResolver();
(3) final ActionBeanContext context = ctx.getActionBeanContext();
(4) final ActionBean actionBean = resolver.getActionBean(context);
(5) final Class beanClass = actionBean.getClass();
(6) final String eventName = resolver.getEventName(beanClass, context);

  1. The Stripes Configuration class holds all the configuration information passed into Stripes from the web.xml. This includes all configured Interceptors.
  2. The ActionResolver will allows us to get which Method was requested from the ActionBean during the request.
  3. The ActionBeanContext is used to help us get an instance of the requested ActionBean
  4. The requested ActionBean.
  5. We need the actual Class.
  6. The eventName or requested method to call.

The next thing we need to do is get the Method or handler if the eventName doesn't exist. The reason for this is because if you request a URL and there is no eventName specified Stripes will look for a default handler which is a method denoted by the @DefaultHandler annotation. And then if this doesn't exist we need to throw an error and inform the user.

Good Practice: All ActionBeans should have a default handler in case an eventName was not given.

final Method handler;
if (eventName != null) {
handler = resolver.getHandler(beanClass, eventName);
} else {
handler = resolver.getDefaultHandler(beanClass);
if (handler != null) {
context.setEventName(resolver.getHandledEvent(handler));
}
}

// Insist that we have a handler
if (handler == null) {
throw new StripesServletException(
"No handler method found for request with ActionBean ["
+ beanClass.getName() + "] and eventName [ "
+ eventName + "]");
}

Don't worry too much about understanding every bit of that. Just know that we need the handler and we need to throw an error if one doesn't exist. The next thing we need to do is actually look for the @NoCache annotation. If we find it we set the no cache header junk and tell the ExecutionContext to proceed with the request.

if (isCachingDisabled(handler, beanClass)) {
HttpServletResponse response = context.getResponse();
response.setDateHeader("Expires", 0);
response.setHeader("Cache-control", "no-cache");
response.setHeader("Pragma", "no-cache");
}
return ctx.proceed();

Now we'll look at the isCachingDisabled method. We pass this method the handler and also the beanClass.

protected boolean isCachingDisabled(Method method, Class beanClass) {
....
}

To make sure that we are as performant as possible Stripes caches interceptor instances. Because of this we can also cache data within the interceptor. In this case once a @NoCache annotation has been found we want to cache this fact so the next time this interceptor is run with this beanClass we can just check the cache and not have to go through the annotation reflection checks. We first want to check the handler (Method) being called because it override class level annotations. If no annotation is found then we check for a class annotation.

So first, lets check the cache:

CacheKey cacheKey = new CacheKey(method, beanClass);
Boolean disabled = cache.get(cacheKey);
if (disabled != null) {
return disabled;
}

Don't worry about CacheKey yet. It's a simple class and we'll discuss it last. So if we found cache we return the value we found. Otherwise, we need to check for the annotation:

NoCache annotation = method.getAnnotation(NoCache.class);
if (annotation != null) {
disabled = annotation.value();
} else {
// search the method's class and its superclasses
Class clazz = beanClass;
do {
annotation = clazz.getAnnotation(NoCache.class);
clazz = clazz.getSuperclass();
} while (clazz != null && annotation == null);

if (annotation != null) {
disabled = annotation.value();
} else {
disabled = false;
}
}

So basically we look for an annotation on the method. If we find it we return its value. Otherwise, we check the class. We also want to check all super classes incase the user is extending a Base ActionBean of sorts.

Good Practice: Creating a BaseActionBean and having all your ActionBeans extend that base class will save a lot of boilerplace code and make developing with Stripes a lot simpler.

So once we either find an annotation or not we want to cache it and then return what we found:

cache.put(cacheKey, disabled);
return disabled;

The last thing for code is the CacheKey class. We want to make sure that we store unique keys when checking for cache. That way we always know we have the correct cache for the correct beanClass that was requested. Here's the class. I just make it an inner class of the interceptor.

private static final class CacheKey {
private Method method;
private Class beanClass;
private int hashCode;

public CacheKey(Method method, Class beanClass) {
super();
this.method = method;
this.beanClass = beanClass;
this.hashCode = method.hashCode() * 37 + beanClass.hashCode();
}

@Override
public boolean equals(Object obj) {
CacheKey that = (CacheKey) obj;
return this.method.equals(that.method)
&& this.beanClass.equals(that.beanClass);
}

@Override
public int hashCode() {
return hashCode;
}

@Override
public String toString() {
return beanClass.getName() + "." + method.getName();
}
}

Note: In Stripes 1.5, yet to be released, there will be a core set of interceptors on by default. This NoCache interceptor will be one of them along with BeforeAfterMethodInterceptor.

And that's it. Now to use it you might do something like this:

public class SomeActionBean extends BaseActionBean {

@NoCache
public Resolution ajaxEvent() {
// return some ajaxy stuff here
}
}

If you needed all but one method to turn caching off you might do something like this:

@NoCache
public class SomeActionBean extends BaseActionBean {

@NoCache(false)
public Resolution ajaxEvent() {
// return some ajaxy stuff here
}
}

I've included the full source for the interceptor and annotation at the following URL.

NoCache.tar.gz


3 comments:

Unknown said...

Great article! One tiny point -- the logic you have in place for finding the event handler is not always necessary at most lifecycle stages, especially the ones that occur after EventHandlerResolution, as Stripes has already found the event handling method. To get that method:

Method eventHandler = executionContext.getHandler();

This will return the default handler if there is one, or the specific one if @HandlesEvent is there..

Gregg Bolinger said...

Thanks Stephen. I've made note of that. Appreciate the feedback.

Anonymous said...

It is extremely interesting for me to read this article. Thanks for it. I like such topics and anything connected to them. I would like to read a bit more soon.
Alex
Phone jammer