10

I'm building an internal library that should automatically add a few controllers to Spring MVC application. These controllers are all @RestController with a few handler methods annotated with @RequestMapping. Since it's a library, I want users to be able to specify the root path where the library should expose all these controllers. Illustration:

// given that I have a controller like this:
@RestController
@RequestMapping("/admin") 
class AdminController {
  @RequestMapping("/users")
  UsersDto allUsers() { ... }
}

// it will be exposed at `http://localhost/admin/users`

What I want to achieve is to make /admin part configurable. For example, if user says in application.properties:

super.admin.path=/top/secret/location/here

I want AdminController's handlers to be available at http://localhost/top/secret/location/here, and so the allUsers() handler should have a full path of:

http://localhost/top/secret/location/here/users

How do I achieve this? Feels like a pretty common task, but I didn't manage to find a straightforward solution that works.

My finding #1

There's a SimpleUrlHandlerMapping mechanism that seems to be exactly what I want:

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
    SimpleUrlHandlerMapping simpleUrlHandlerMapping = new SimpleUrlHandlerMapping();
    simpleUrlHandlerMapping.setOrder(Integer.MAX_VALUE - 2);
    simpleUrlHandlerMapping.setUrlMap(Collections.singletonMap("/ANY_CUSTOM_VALUE_HERE/*", "adminController"));
    return simpleUrlHandlerMapping;
}

But it keeps saying

The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler

My finding #2

There's Spring Boot Admin project that has this exact feature - you may configure where they should expose all their endpoints. They seem to have this functionality implemented from scratch in PrefixHandlerMapping. How they use it:

...
@Bean
public PrefixHandlerMapping prefixHandlerMappingNotificationFilterController() {
    PrefixHandlerMapping prefixHandlerMapping = new PrefixHandlerMapping(notificationFilterController());
    prefixHandlerMapping.setPrefix(adminServerProperties.getContextPath());
    return prefixHandlerMapping;
}
...
Andrey Agibalov
  • 7,624
  • 8
  • 66
  • 111
  • 2
    The `SimpleUrlHandlerMapping` won't work for `@(Rest)Controller`. 2 is part of a 3rd party project and will map all your controllers. Just put the admin stuff in a different `DispatcherServlet` map `/admin` (or whatever they want) to that `DispatcherServlet` and be done with it. – M. Deinum May 29 '16 at 18:22

1 Answers1

14

In addition to @M. Deinum solution, you can use Path Patterns with Placeholders. As Spring documentation states:

Patterns in @RequestMapping annotations support ${…​} placeholders against local properties and/or system properties and environment variables. This may be useful in cases where the path a controller is mapped to may need to be customized through configuration.

So in your case, your controller would be like:

@RestController
@RequestMapping("/${super.admin.path:admin}") 
class AdminController {
  // Same as before
}

The preceding controller would use super.admin.path local/system property or environment variable value as its prefix or admin if those aren't provided. If you're using Spring Boot, by adding the following to your application.properties:

super.admin.path=whatever

You can customize that prefix.

Ali Dehghani
  • 46,221
  • 15
  • 164
  • 151
  • 1
    Profiles do synergize very well with what you describe: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-properties-and-configuration.html#howto-change-configuration-depending-on-the-environment – sashok_bg May 30 '16 at 07:58
  • I tried the solution, but it doesn't work for me. The property within the value isn't resolved – JimHawkins Jan 20 '17 at 10:20
  • Are using Spring Boot? Or plain Spring MVC? – Ali Dehghani Jan 20 '17 at 11:00
  • Is there any chance to combine this with messages*.properties to build i18n url mappings? – StephanM Jul 14 '17 at 10:01
  • great solution, worked for us, but another problem arose during testing. When testing our controller using MockMvc, it started to throw exception - invalid mapping. Apparently it cannot find the injected variable in MockMvc – JavaTec Jun 20 '19 at 21:49