Issue
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?
Solution
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:
@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.@RefreshScope
doesn't work nicely with filters either, as seen in this issue.
That means you have two options:
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.
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 theEnvironment
itself still requires you to use the/actuator/refresh
endpoint.
Answered By - g00glen00b