Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Enable and disable endpoints at runtime with Spring boot

Let's say I have the following controller:

@RestController
public class MyController {
    @GetMapping("v1/remain")
    public MyObject getRemain() {
        // ...
    }
}

How can I enable or disable this endpoint at runtime dynamically with Spring boot? Also, is it possible to change this without having to restart the application?

like image 850
arrizo Avatar asked Jun 02 '19 11:06

arrizo


2 Answers

You can either use @ConditionalOnExpression or @ConditionalOnProperty

@RestController
@ConditionalOnExpression("${my.property:false}")
@RequestMapping(value = "my-end-point", produces = MediaType.APPLICATION_JSON_VALUE)
public class MyController {
    @RequestMapping(value = "endpoint1", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> endpoint1(
        return new ResponseEntity<>("Hello world", HttpStatus.OK);
    }
}

Now if you want the above controller to work, you need to add following in application.properties file.

my.controller.enabled=true

Without the above statement, it will behave like the above controller don't exist.

Similiarly,

@ConditionalOnProperty("my.property")

behaves exactly same as above; if the property is present and "true", the component works, otherwise it doesn't.

like image 117
Malav Shah Avatar answered Sep 20 '22 13:09

Malav Shah


To dynamically reload beans when a property changes, you could use Spring boot actuator + Spring cloud so that you have access to the /actuator/refresh endpoint.

This can be done by adding the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
</dependency>

The latter does require that you add the BOM for Spring cloud, which is:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Now you can enable the /actuator/refresh endpoint by setting the following property:

 management.endpoints.web.exposure.include=refresh

This will allow you to send a POST call to /actuator/refresh, which will return an array of all changed properties.


By using the /actuator/refresh endpoint, it also allows you to use the @RefreshScope annotation to recreate beans. However, there are a few limitations:

  1. @RefreshScope recreates the bean without re-evaluating conditionals that might have changed due to the refresh. That means that this solution doesn't work with @RefreshScope, as seen in the comment section of this question.
  2. @RefreshScope doesn't work nicely with filters either, as seen in this issue.

That means you have two options:

  1. Add the @RefreshScope to the controller and do the conditional logic by yourself, for example:

    @RefreshScope
    @RestController
    @RequestMapping("/api/foo")
    public class FooController {
        @Value("${foo.controller.enabled}")
        private boolean enabled;
    
        @GetMapping
        public ResponseEntity<String> getFoo() {
            return enabled ? ResponseEntity.of("bar") : ResponseEntity.notFound().build();
        }
    }
    

    This means you would have to add this condition to all endpoints within your controller. I haven't verified if you could use this with aspects.

  2. Another solution is to not use @RefreshScope to begin with, and to lazily fetch the property you want to validate. This allows you to use it with a filter, for example:

    public class FooFilter extends OncePerRequestFilter {
        private Environment environment;
    
        public FooFilter(Environment environment) {
            this.environment = environment;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            if ("true".equalsIgnoreCase(environment.getProperty("foo.controller.enabled"))) {
                filterChain.doFilter(request, response);
            } else {
                response.setStatus(HttpStatus.NOT_FOUND.value());
            }
        }
    }
    

    You'll have to register the filter as well, for example by using:

    @Bean
    public FilterRegistrationBean<FooFilter> fooFilter(Environment environment) {
        FilterRegistrationBean<FooFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new FooFilter(environment));
        bean.addUrlPatterns("/api/foo");
        return bean;
    }
    

    Please note, this approach only fetches the property dynamically from the Environment. Refreshing the Environment itself still requires you to use the /actuator/refresh endpoint.

like image 23
g00glen00b Avatar answered Sep 19 '22 13:09

g00glen00b