37

I'm using spring 2.5 and annotations to configure my spring-mvc web context. Unfortunately, I am unable to get the following to work. I'm not sure if this is a bug (seems like it) or if there is a basic misunderstanding on how the annotations and interface implementation subclassing works.

For example,

@Controller
@RequestMapping("url-mapping-here")
public class Foo {
  @RequestMapping(method=RequestMethod.GET)
  public void showForm() {
    ...
  }
  @RequestMapping(method=RequestMethod.POST)
  public String processForm() {
  ...
  }
}

works fine. When the context starts up, the urls this handler deals with are discovered, and everything works great.

This however does not:

@Controller
@RequestMapping("url-mapping-here")
public class Foo implements Bar {
  @RequestMapping(method=RequestMethod.GET)
  public void showForm() {
    ...
  }
  @RequestMapping(method=RequestMethod.POST)
  public String processForm() {
  ...
  }
}

When I try to pull up the url, I get the following nasty stack trace:

javax.servlet.ServletException: No adapter for handler [com.shaneleopard.web.controller.RegistrationController@e973e3]: Does your handler implement a supported interface like Controller?
    org.springframework.web.servlet.DispatcherServlet.getHandlerAdapter(DispatcherServlet.java:1091)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:874)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:809)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:571)
    org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:501)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:627)

However, if I change Bar to be an abstract superclass and have Foo extend it, then it works again.

@Controller
@RequestMapping("url-mapping-here")
public class Foo extends Bar {
  @RequestMapping(method=RequestMethod.GET)
  public void showForm() {
    ...
  }
  @RequestMapping(method=RequestMethod.POST)
  public String processForm() {
  ...
  }
}

This seems like a bug. The @Controller annotation should be sufficient to mark this as a controller, and I should be able to implement one or more interfaces in my controller without having to do anything else. Any ideas?

leppie
  • 115,091
  • 17
  • 196
  • 297
layne
  • 443
  • 2
  • 5
  • 7

6 Answers6

13

What I needed to do was replace

 <tx:annotation-driven/>

with

 <tx:annotation-driven  proxy-target-class="true"/>

This forces aspectj to use CGLIB for doing aspects instead of dynamic proxies - CGLIB doesn't lose the annotation since it extends the class, whereas dynamic proxies just expose the implemented interface.

James Kingsbery
  • 7,298
  • 2
  • 38
  • 67
12

Ed is right, adding

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

works fine

Michal Bachman
  • 2,661
  • 17
  • 22
10

If you wish to use interfaces for your Spring MVC controllers then you need to move the annotations around a bit, as mentioned in the Spring docs: http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/mvc.html#mvc-ann-requestmapping

Using @RequestMapping On Interface Methods A common pitfall when working with annotated controller classes happens when applying functionality that requires creating a proxy for the controller object (e.g. @Transactional methods). Usually you will introduce an interface for the controller in order to use JDK dynamic proxies. To make this work you must move the @RequestMapping annotations to the interface as well as the mapping mechanism can only "see" the interface exposed by the proxy. Alternatively, you could activate proxy-target-class="true" in the configuration for the functionality applied to the controller (in our transaction scenario in ). Doing so indicates that CGLIB-based subclass proxies should be used instead of interface-based JDK proxies. For more information on various proxying mechanisms see Section 8.6, “Proxying mechanisms”.

Unfortunately it doesn't give a concrete example of this. I have found a setup like this works:

@Controller
@RequestMapping(value = "/secure/exhibitor")
public interface ExhibitorController {

    @RequestMapping(value = "/{id}")
    void exhibitor(@PathVariable("id") Long id);
}

@Controller
public class ExhibitorControllerImpl implements ExhibitorController {

    @Secured({"ROLE_EXHIBITOR"})
    @Transactional(readOnly = true)
    @Override
    public void exhibitor(final Long id) {

    }
}

So what you have here is an interface that declares the @Controller, @PathVariable and @RequestMapping annotations (the Spring MVC annotations) and then you can either put your @Transactional or @Secured annotations for instance on the concrete class. It is only the @Controller type annotations that you need to put on the interface because of the way Spring does its mappings.

Note that you only need to do this if you use an interface. You don't necessarily need to do it if you are happy with CGLib proxies, but if for some reason you want to use JDK dynamic proxies, this might be the way to go.

Kieran
  • 5,906
  • 3
  • 24
  • 34
5

There's no doubt that annotations and inheritance can get a little tricky, but I think that should work. Try explicitly adding the AnnotationMethodHandlerAdapter to your servlet context.

http://static.springframework.org/spring/docs/2.5.x/reference/mvc.html#mvc-ann-setup

If that doesn't work, a little more information would be helpful. Specifically, are the two annotated controller methods from the interface? Is Foo supposed to be RegistrationController?

Ed Thomas
  • 1,153
  • 1
  • 12
  • 21
5

I know it is too late but i'm writing this for anyone have this problem if you are using annotation based configuration... the solution might be like this:

@Configuration
@ComponentScan("org.foo.controller.*")
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class AppConfig { ...}
Amir
  • 652
  • 9
  • 19
0

The true reason you need to use 'proxy-target-class="true"' is in DefaultAnnotationHandlerMapping#determineUrlsForHandler() method: though it uses ListableBeanFactory#findAnnotationOnBean for looking up a @RequestMapping annotation (and this takes care about any proxy issues), the additional lookup for @Controller annotation is done using AnnotationUtils#findAnnotation (which does not handles proxy issues)

Boris Kirzner
  • 895
  • 10
  • 13