Spring MVC @RequestMapping Inheritance
Asked Answered
I

2

42

Coming from Struts2 I'm used to declaring @Namespace annotation on super classes (or package-info.java) and inheriting classes would subsequently pick up on the value in the @Namespace annotation of its ancestors and prepend it to the request path for the Action. I am now trying to do something similar in Spring MVC using @RequestMapping annotation as follows (code trimmed for brevity):

package au.test

@RequestMapping(value = "/")
public abstract class AbstractController {
    ...
}

au.test.user

@RequestMapping(value = "/user")
public abstract class AbstractUserController extends AbstractController {

    @RequestMapping(value = "/dashboard")   
    public String dashboard() {
        ....
    }
}

au.test.user.twitter

@RequestMapping(value = "/twitter")
public abstract class AbstractTwitterController extends AbstractUserController {
    ...
}

public abstract class TwitterController extends AbstractTwitterController {

    @RequestMapping(value = "/updateStatus")    
    public String updateStatus() {
        ....
    }
}
  • / works as expect
  • /user/dashboard works as expected
  • However when I would have expected /user/twitter/updateStatus to work it does not and checking the logs I can see a log entry which looks something like:

org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping - Mapped URL path [/tweeter/updateStatus] onto handler 'twitterController'

Is there a setting I can enable that will scan the superclasses for @RequestMapping annotations and construct the correct path?

Also I take it that defining @RequestMapping on a package in package-info.java is illegal?

Ileus answered 11/3, 2011 at 3:28 Comment(0)
A
34

The following basically becomes /tweeter/updateStatus and not /user/tweeter/updateStatus

public abstract class TwitterController extends AbstractTwitterController {

    @RequestMapping(value = "/updateStatus")    
    public String updateStatus() {
        ....
    }
}

That's the expected behavior since you've overriden the original @RequestMapping you've declared in the AbstractController and AbstractUserController.

In fact when you declared that AbstractUserController it also overriden the @RequestMapping for AbstractController. It just gives you the illusion that the / from the AbstractController has been inherited.

"Is there a setting I can enable that will scan the superclasses for @RequestMapping annotations and construct the correct path?" Not that I know of.

Adigun answered 11/3, 2011 at 4:9 Comment(4)
Have you by any chance got any other suggestions as to how to achieve this. Just seems to me like a waste having to put /x/y/z in a sub class environment. And then if I decided to change /x/ to /k/ I have to go to all the place in the code where it is defined and change it manually!Ileus
@chris: This doesn't work for me. Is there a configuration step involved, or is it just an annotations issue? The base controller is indeed in the same package as the controller extending the base controller. However, the base controller's URI/RequestMapping is ignored and/or not found. "org.springframework.web.servlet.DispatcherServlet:947 - No mapping found for HTTP request with URI". So, if I just use the extended controller's "base" RequestMapping, it's fine. If I try to use the RequestMapping inherited by the base controller, it cannot find the URI.Undercover
Its worth stating explicitly that super-class paths are still routable. That is, overriding a class and providing new @RequestMappings does not override or replace the super-class's @RequestMappings, it just adds additional ones. In the OP's question above, for example, that means all of these paths are valid: /user/dashboard, /twitter/updateStatus.Intercollegiate
that would be twitter, not tweeter. :)Equivalent
R
11

According to the technique explained in Modifying @RequestMappings on startup, yes, it's possible to construct a URL pattern from superclasses in a way you want.

In essence, you have to subclass RequestMappingHandlerMapping (most likely, it will be your HandlerMapping implementation, but please check first) and override protected getMappingForMethod method. Once this renders to be feasible, you are in full control of URL pattern generation.

From the example you gave it's not completely clear the exact merging policy, for example, what path you want to have if a superclass AbstractTwitterController also implements updateStatus() method with its own @RequestMapping, or how would you like to concatenate the URL patterns across the hierarchy, top-down or bottom-up, (I assumed the former below), but, hopefully, the following snippet will give you some ideas :

    private static class PathTweakingRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

                @Override
                protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
                    RequestMappingInfo methodMapping = super.getMappingForMethod(method, handlerType);
                    if (methodMapping == null)
                        return null;
                    List<String> superclassUrlPatterns = new ArrayList<String>();
                    boolean springPath = false;
                    for (Class<?> clazz = handlerType; clazz != Object.class; clazz = clazz.getSuperclass())
                        if (clazz.isAnnotationPresent(RequestMapping.class))
                            if (springPath)
                                superclassUrlPatterns.add(clazz.getAnnotation(RequestMapping.class).value()[0]);// TODO handle other elements in the array if necessary
                            else
                                springPath = true;
                    if (!superclassUrlPatterns.isEmpty()) {
                        RequestMappingInfo superclassRequestMappingInfo = new RequestMappingInfo("",
                                new PatternsRequestCondition(String.join("", superclassUrlPatterns)), null, null, null, null, null, null);// TODO implement specific method, consumes, produces, etc depending on your merging policies
                        return superclassRequestMappingInfo.combine(methodMapping);
                    } else
                        return methodMapping;
                }
    }

Another good question is how to intercept the instantiation of RequestMappingHandlerMapping. In the Internet there are quite a number of various examples for various configuration strategies. With JavaConfig, however, remember that if you provide WebMvcConfigurationSupport in your @Configuration set, then your @EnableWebMvc(explicit or implicit) will stop to work. I ended up with the following:

@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration{

    @Configuration
    public static class UnconditionalWebMvcAutoConfiguration extends WebMvcAutoConfiguration {//forces @EnableWebMvc 
    }

    @Override
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new PathTweakingRequestMappingHandlerMapping();
    }

    @Bean
    @Primary
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() { 
        return super.requestMappingHandlerMapping();
    }

}

but would like to learn about better ways.

Reitz answered 23/11, 2016 at 18:2 Comment(2)
@RequestMapping(value = "updateStatus", inheritSuperclass = true) would be convenientFachanan
True. Many ways to do it: annotate superclass method with something like @Inheritable or even imply it by method overriding. Then the question is why the creators of Spring MVC didn't provide Strut-like inheritable mapping out of the box? Maybe they decided that if Request Mapping is configurable, then it is up to the developers.Reitz

© 2022 - 2024 — McMap. All rights reserved.