@HandlerInterceptor for Spring MVC
I have been doing quite a bit of Spring MVC at work these days. I like the @Controller model and with the bunch of
enhancements that have gone into Spring 3.0, Spring MVC has the potential to become the defacto Action based framework.
While Spring MVC has excellent support for Controller Annotations to cut down on amount of the XML
configuration, you are still expected to configure HandlerInterceptor -s in the Spring configuration file.
Sometimes it helps to keep all the related code together in one artifact.
So let me quickly summarize what I did -
I defined a custom annotation @Interceptors that lets you specify the interceptors that you would like to apply on
the controller.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Interceptors {
/** HandlerInterceptor**/
Class[] value();
}
The custom @Interceptors annotation is exactly same as the EJB 3.0 Interceptors annotation definition. I kind of liked the fact that I could navigate to the interceptor implementation class using the IDE.
Next you define a couple of interceptors like so -
class MyInterceptor1 implements HandlerInterceptor{
//..
}
class MyInterceptor2 extends HandlerInterceptorAdapter{
//..
}
and annotate the @Controller-s -
@Controller
@Interceptors({MyInterceptor1.class,MyInterceptor2.class})
class TestController{
@RequestMapping("/test")
void test(ModelMap mm){
}
}
Next let’s look at the implementation -
Spring’s HandlerMapping is responsible for resolving the HandlerExectionChain that maps to a given request URL. The HandlerExecutionChain encapuslates the Controller and the associated interceptors for a given request URL.
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
Spring’s DefaultAnnotationHandlerMapping class maps handlers/@Controllers based on HTTP paths expressed through the RequestMapping annotation at the type or method level.
So we will enhance DefaultAnnotationHandlerMapping and add the ability to detect @Interceptors on the controllers
package com.springmvc.extensions;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping;
public class HandlerInterceptorAnnotationAwareHandlerMapping extends DefaultAnnotationHandlerMapping {
@Override
protected HandlerExecutionChain getHandlerExecutionChain(Object handler,HttpServletRequest request){ #1
HandlerExecutionChain chain = super.getHandlerExecutionChain(handler,request); #2
HandlerInterceptor[] interceptors = detectInterceptors(chain.getHandler().getClass());
chain.addInterceptors(interceptors);
return chain;
}
protected HandlerInterceptor[] detectInterceptors(Class handlerClass) {
Interceptors interceptorAnnot = AnnotationUtils.findAnnotation(handlerClass, Interceptors.class); #3
List interceptors = new ArrayList();
if (interceptorAnnot != null) {
Class[] interceptorClasses = interceptorAnnot.value();
if (interceptorClasses != null) {
for (Class interceptorClass : interceptorClasses) { #4
if (!HandlerInterceptor.class.isAssignableFrom(interceptorClass)) {
raiseIllegalInterceptorValue(handlerClass,interceptorClass); #5
}
interceptors.add((HandlerInterceptor) BeanUtils.instantiateClass(interceptorClass)); #6
}
}
}
return interceptors.toArray(new HandlerInterceptor[0]);
}
protected void raiseIllegalInterceptorValue(Class handlerClass,Class interceptorClass) {
throw new IllegalArgumentException(interceptorClass + " specified on "
+ handlerClass + " does not implement " + HandlerInterceptor.class.getName());
}
}
#1 Override the getHandlerExecutionChain() method defined in DefaultAnnotationHandlerMapping
#2 Call ‘super()’ so that you don’t change the base class behavior . This way you can aggregate the interceptors configured
in the Spring context file against the property ‘interceptors’ as well (it at all configured) .
#3 Look for @Interceptors annotation for a given Controller class.
#4 #5 Run through all the specified HandlerInterceptor implementations , making sure that they indeed implement the Spring’s
HandlerInterceptor interface.
#6 Instantiate the HandlerInterceptor instance (This will look for a default no-arg contructor). You could look up the
Spring ApplicationContext and check if an Interceptor has been configured as well.
As a final step, you need to declare the HandlerInterceptorAnnotationAwareHandlerMapping class in the Spring Application Context
<bean class="com.springmvc.extensions.HandlerInterceptorAnnotationAwareHandlerMapping "/>
Let me know what you think.
Update: Nice to see keith link to this blog on spring jira
some good stuff! thanks.
Very interesting article. I tried to implement your approach. However the final step seems to be incomplete. I didn’t manage to have my controller annotations with Intersectors be handled at all. Could u provide more details about this final step, how it works ? I am using the latest release of Spring 3.0 with Roo.
You need to make sure that the custom HandlerMapping is used by Spring dispatcher servlet to resolve @Controller-s.
If you are using spring 3.0 MVC namespace ( <mvc:annotation-driven/>, it automatically registers the default DefaultAnnotationHandlerMapping. In that case, HandlerInterceptorAnnotationAwareHandlerMapping will not be used by DispatcherServlet.
So Interceptors will not be detected.
So you could comment out <mvc:annotation-driven/> and give it a shot.
I’m assuming that you are “component-scanning” your @Controller-s
- Karthik
I found I could leave the annotation-driven, so long as I explicitly set the “order” of my customer handler mapping to -1. It’s kinda weird, but the annotation-driven parser explicitly creates, and sets the default handler to 0, and provides no way to directly override. However, since it does honor order, this seems to work fine.
Thanks karthik. I was looking for something like this
[...] for further discussion/examples on the topic: http://karthikg.wordpress.com/2009/10/12/athandlerinterceptor-for-spring-mvc/ [...]
[...] for further discussion/examples on the topic: http://karthikg.wordpress.com/2009/10/12/athandlerinterceptor-for-spring-mvc/ [...]
Nice idea, apart from BeanUtils.instantiateClass(interceptorClass). What if my interceptor needs injected dependencies? Is there a way I can get a bean from the context?
It seems that the DefaultAnnotationHandlerMapping already has access to the full context. So you can replace the instantiateClass call with getApplicationContext().getBean(interceptorClass) if you need beans that have been autowired already.
Yep, we do both. Look for the class in the application context and if not found , instantiate it.
- Karthik
Great and very useful post.
Thanks!
I know this is an old post but I found it very usefull so : thank you.
I just modify it to avoid the @Interceptors annotation and add all the beans that implement the HandlerInterceptor (you loose the order but if you do not need it…) :
protected HandlerInterceptor[] detectInterceptors() {
Map interceptors = this.getApplicationContext().getBeansOfType(HandlerInterceptor.class);
Collection interceptors_ = interceptors.values();
return interceptors_.toArray(new HandlerInterceptor[interceptors_.size()]);
}
@Chris.
I thought the point was to keep the class and annotation together so when you read the class you can understand it is intercepted.
Anybody tried adding checks for method level interceptors. I don’t need it today, but I can see people asking (my short term answer will be to tell them just to make the call directly in the method, but it would be cleaner if we could use the exact same syntax at both method and class level)
Hi,
I’ve changed it a bit, so instead of using class in annotation, I use bean name (so it can be set in spring context with dependencies if required), then in handler interceptor “getApplicationContext().getBean(…)”
What if you want an interceptor to run for all requests, or better yet, only “get” requests?
Thanks karthik, this post is very useful. I have just changed the following line :
interceptors.add((HandlerInterceptor) BeanUtils.instantiateClass(interceptorClass));
by:
interceptors.add((HandlerInterceptor) getApplicationContext().getBean(interceptorClass));
In order to get interceptors alreadt fully instantiated by spring.
Thanks for sharing an Interesting and an useful post..
Thanks Karthik, this post is very useful. I would like to implement this on action method level as well. Has anyone tried that?