39
votes

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?

2

2 Answers

31
votes

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.

10
votes

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.