Mathias Hauser
My opinions and thoughts on various things and experiences of software development
Spring RestController specific basePath
October 3, 2016
Posted by on I recently started taking a look into Angular 2 and though about a proper project setup in combination with Spring RestControllers. During development, the front-end Angular 2 application will run via Webpack and the Spring Boot application with an embedded Tomcat. All RestController should use a common base path, since I plan to run the Angular 2 application together with the Spring application in a production environment. The test scenario is a simple index.html file that should be hosted in the URL ‘/index.html’ and RestController hosted in ‘/api/hello’
Expected URL structure
/index.html – angular app placeholder
/api/hello – Endpoint for a rest controller
I recommend downloading the source of this example before you start. Link to the repository blog_rest-controller-base-path direct link to the source zip source-zip
Content of this post
- Spring configuration for the base path of all RestControllers
- Custom annotation that combines @RestController and @RequestMapping
Spring configuration
The starting point is a Spring Boot project generated with start.spring.io with just the web dependency.
The only necessary configuration is a bean of type WebMvcRegistrationsAdapter that was introduced with Spring Boot 1.4.0. This class allows to define a custom implementation of the RequestMappingHandlerMapping via overriding ‘getRequestMappingHanlderMapping’. As usual with Spring Boot we can declare such a bean in any class that is annotation with @Configuration.
In this implementation we check if the declaring annotation is annotated with @RestController. It is a good approach to use the AnnotationUtils class for this check since it traverses interfaces, annotations, and superclasses.
In this implementation we define a new pattern that starts with ‘api’ and is followed by the existing patter. If the @RestController annotation is present.
@Configuration public class WebConfig { @Bean public WebMvcRegistrationsAdapter webMvcRegistrationsHandlerMapping() { return new WebMvcRegistrationsAdapter() { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new RequestMappingHandlerMapping() { private final static String API_BASE_PATH = "api"; @Override protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { Class<?> beanType = method.getDeclaringClass(); if (AnnotationUtils.findAnnotation(beanType, RestController.class) != null) { PatternsRequestCondition apiPattern = new PatternsRequestCondition(API_BASE_PATH) .combine(mapping.getPatternsCondition()); mapping = new RequestMappingInfo(mapping.getName(), apiPattern, mapping.getMethodsCondition(), mapping.getParamsCondition(), mapping.getHeadersCondition(), mapping.getConsumesCondition(), mapping.getProducesCondition(), mapping.getCustomCondition()); } super.registerHandlerMethod(handler, method, mapping); } }; } }; } }
The following controller is used to test if the our controllers are now hosted relative to the ‘api’ path.
@RestController @RequestMapping("hello") public class HelloController { @RequestMapping public Hello all() { Hello hello = new Hello(); hello.setText("Hello api"); return hello; } }
We expect that static content will still be available in the root path ‘/’
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello page</title> </head> <body> Hello world! </body> </html>
If we run this application as expect the URL ‘/index.html’ displays ‘Hello World’ and the URL ‘/api/hello’ displays ‘{“text”:”Hello api”}’
Custom annotation combining @RestController and @RequestMapping
Once you write multiple controllers, it is necessary to annotated all of them with @RestController and @RequestMapping(“subpath”). This can combined into a custom annotation. I named it @RestApiController that expects a parameter for the subpath similar to @RequestMapping.
The @AliasFor annotation introduced with Spring 4.2 allows to pass the parameter to another annotation. For our case we pass the value from @RestApiController to @RequestMapping
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @RestController @RequestMapping public @interface RestApiController { @AliasFor(annotation = RequestMapping.class, attribute = "value") String[] value(); }
The modified controller using this annotation looks like the following.
@RestApiController("hello") public class HelloController { @RequestMapping public Hello all() { Hello hello = new Hello(); hello.setText("Hello api"); return hello; } }
Questions
Feel free to ask any questions about this post and I will try to answer them in the comments or via mail -> dev[at]mathias-hauser.at.
You can also send me a mail if you have a suggestion for another post.
First I would like to thank you for a wonderful post. Thanks. One suggestion though, if you would add default value in @RestApiController then the value will not be mandatory when decorating the class with annotation and it will still work with basePath prepended to all rest controllers. For example.
Thank you for this article