javaspringspring-boot

SpringBoot RestController @GetMapping route configurable


I want to be able to configure the @GetMapping route with some properties.

We have a service whose business logic is the same. But we deploy it in different companies, and each of the companies has its rest API routes.

For example, the company A would like our service exposed to /api/v1.1/bar

Company B would like our service exposed to: /api/foo

etc

Therefore, we went to try this piece of code:

@RestController
public class DummyController {

    @Value("${configuration.route}")
    private String route;

    @GetMapping(value = route)
    String doSomeBusinessLogic() {
        doSomeVeryCoolBusinessLogic();

Expected:

With the code above, we were expecting that it would work, making the route configurable

Actual: we are getting this compilation error: Attribute value must be constant

The current workaround, we are creating an N version of the same apps, just to change one line, the get mapping line

App for company A

@GetMapping(value = "/api/v1.1/bar")
String doSomeBusinessLogic() {
    doSomeVeryCoolBusinessLogic();

app for company B

@GetMapping(value = "/api/foo")
String doSomeBusinessLogic() {
    doSomeVeryCoolBusinessLogic();

We believe this is not the way to go, therefore:

Question:

how to make the route configurable with SpringBoot ?


Solution

  • I am not saying this is necessarily a good idea :) but here are some options

    1. Use Router Function

    You should of course consider using RouterFunctions instead. But if for whatever reason you don't want to use option #2

    2. Update the route with BeanPostProcessor

    The problem is that the annotations (like @GetMapping) have to have constant values, therefore hard to change at runtime. We can use a BeanPostProcessor to do that:

    @Component
    public class DemoBeanPostProcessor implements BeanPostProcessor {
    
        @Value("${configuration.route}")
        private String newUrl;
    
        @SneakyThrows
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) {
            if (bean instanceof DummyController myController) {
                GetMapping annotation = myController.getClass().getAnnotation(GetMapping.class);
                InvocationHandler handler = Proxy.getInvocationHandler(annotation);
                Field field = handler.getClass().getDeclaredField("memberValues");
                field.setAccessible(true);
                Map<String, Object> memberValues = (Map) field.get(handler);
                memberValues.put("value", newUrl);
                return bean;
            }
    
            return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
        }
    }
    

    since we are changing private members, for this to work you would need to allow reflection with:

    --add-opens=java.base/sun.reflect.annotation=ALL-UNNAMED