1. Overview
Endpoints in a Spring Boot application are the mechanisms to interact with it. At times such as during an unplanned maintenance window, we might want to temporarily limit the application’s interactions with the outside.
In this tutorial, we’ll learn to enable and disable endpoints at runtime in a Spring Boot application using a few popular libraries, such as Spring Cloud, Spring Actuator, and Apache’s Commons Configuration.
2. Setup
In this section, let’s focus on setting up critical aspects for our Spring Boot project.
2.1. Maven Dependencies
First, we’ll need our Spring Boot application to expose the /refresh endpoint, so let’s add the spring-boot-starter-actuator dependency in the project’s pom.xml file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.1.5</version>
</dependency>
Next, since we’ll need the @RefreshScope annotation later for reloading the property sources in the environment, let’s add the spring-cloud-starter dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>3.1.5</version>
</dependency>
Further, we must also add the BOM for Spring Cloud in the dependency management section of the project’s pom.xml file so that Maven uses a compatible version of spring-cloud-starter:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Finally, as we’ll need the capability to reload a file at runtime, let’s also add the commons-configuration dependency:
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
2.2. Configuration
First, let’s add the configuration to the application.properties file to enable the /refresh endpoint in our application:
management.server.port=8081
management.endpoints.web.exposure.include=refresh
Next, let’s define an additional source that we can use to reload properties:
dynamic.endpoint.config.location=file:extra.properties
Additionally, let’s define the spring.properties.refreshDelay property in the application.properties file:
spring.properties.refreshDelay=1
Finally, let’s add two properties to the extra.properties file:
endpoint.foo=false
endpoint.regex=.*
In the later sections, we’ll understand the core significance of these additional properties.
2.3. API Endpoints
First, let’s define a sample GET API available at /foo path:
@GetMapping("/foo")
public String fooHandler() {
return "foo";
}
Next, let’s define two more GET APIs available at the /bar1 and /bar2 paths, respectively:
@GetMapping("/bar1")
public String bar1Handler() {
return "bar1";
}
@GetMapping("/bar2")
public String bar2Handler() {
return "bar2";
}
In the following sections, we’ll learn how to toggle an individual endpoint, such as /foo. Moreover, we’ll also see how to toggle a group of endpoints, namely /bar1 and /bar2, identifiable through a simple regex.
2.4. Configuring DynamicEndpointFilter
To toggle a group of endpoints at runtime, we can use a Filter. By matching the request’s endpoint using the endpoint.regex pattern, we can allow it on success or send the 503 HTTP response status for an unsuccessful match.
So, let’s define the DynamicEndpointFilter class by extending the OncePerRequestFilter:
public class DynamicEndpointFilter extends OncePerRequestFilter {
private Environment environment;
// ...
}
Further, we need to add the logic of pattern matching by overriding the doFilterInternal() method:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String path = request.getRequestURI();
String regex = this.environment.getProperty("endpoint.regex");
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(path);
boolean matches = matcher.matches();
if (!matches) {
response.sendError(HttpStatus.SERVICE_UNAVAILABLE.value(), "Service is unavailable");
} else {
filterChain.doFilter(request,response);
}
}
We must note the initial value of the endpoint.regex property is “*.**” that allows all the requests through this Filter.
3. Toggle Using Environment Properties
In this section, we’ll learn how to do a hot reload of the environment properties from the extra.properties file.
3.1. Reloading Configuration
For this, let’s start by defining a bean for PropertiesConfiguration using the FileChangedReloadingStrategy:
@Bean
@ConditionalOnProperty(name = "dynamic.endpoint.config.location", matchIfMissing = false)
public PropertiesConfiguration propertiesConfiguration(
@Value("${dynamic.endpoint.config.location}") String path,
@Value("${spring.properties.refreshDelay}") long refreshDelay) throws Exception {
String filePath = path.substring("file:".length());
PropertiesConfiguration configuration = new PropertiesConfiguration(
new File(filePath).getCanonicalPath());
FileChangedReloadingStrategy fileChangedReloadingStrategy = new FileChangedReloadingStrategy();
fileChangedReloadingStrategy.setRefreshDelay(refreshDelay);
configuration.setReloadingStrategy(fileChangedReloadingStrategy);
return configuration;
}
We must note that the source of the properties is derived using the dynamic.endpoint.config.location property in the application.properties file. Additionally, the reload happens with a time delay of 1 second, as defined by the spring.properties.refreshDelay property.
Next, we need to read the endpoint-specific properties at runtime. So, let’s define the EnvironmentConfigBean with property-getters:
@Component
public class EnvironmentConfigBean {
private final Environment environment;
public EnvironmentConfigBean(@Autowired Environment environment) {
this.environment = environment;
}
public String getEndpointRegex() {
return environment.getProperty("endpoint.regex");
}
public boolean isFooEndpointEnabled() {
return Boolean.parseBoolean(environment.getProperty("endpoint.foo"));
}
public Environment getEnvironment() {
return environment;
}
}
Next, let’s create a FilterRegistrationBean to register the DynamicEndpointFilter:
@Bean
@ConditionalOnBean(EnvironmentConfigBean.class)
public FilterRegistrationBean<DynamicEndpointFilter> dynamicEndpointFilterFilterRegistrationBean(
EnvironmentConfigBean environmentConfigBean) {
FilterRegistrationBean<DynamicEndpointFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DynamicEndpointFilter(environmentConfigBean.getEnvironment()));
registrationBean.addUrlPatterns("*");
return registrationBean;
}
3.2. Verification
First, let’s run the application and access the /bar1 or /bar2 APIs:
$ curl -iXGET http://localhost:9090/bar1
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 4
Date: Sat, 12 Nov 2022 12:46:32 GMT
bar1
As expected, we get the 200 OK HTTP response as we have kept the initial value of the endpoint.regex property to enable all the endpoints.
Next, let’s enable only the /foo endpoint by changing the endpoint.regex property in the extra.properties file:
endpoint.regex=.*/foo
Moving on, let’s see if we’re able to access the /bar1 API endpoint:
$ curl -iXGET http://localhost:9090/bar1
HTTP/1.1 503
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 12 Nov 2022 12:56:12 GMT
Connection: close
{"timestamp":1668257772354,"status":503,"error":"Service Unavailable","message":"Service is unavailable","path":"/springbootapp/bar1"}
As expected, the DynamicEndpointFilter disabled this endpoint and sent an error response with HTTP 503 status code.
Finally, we can also check that we’re able to access the /foo API endpoint:
$ curl -iXGET http://localhost:9090/foo
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 3
Date: Sat, 12 Nov 2022 12:57:39 GMT
foo
Perfect! It looks like we’ve got this right.
4. Toggle Using Spring Cloud and Actuator
In this section, we’ll learn an alternative approach of using the @RefreshScope annotation and the actuator /refresh endpoint to toggle the API endpoints at runtime.
4.1. Endpoint Configuration With @RefreshScope
First, we need to define the configuration bean for toggling the endpoint and annotate it with @RefreshScope:
@Component
@RefreshScope
public class EndpointRefreshConfigBean {
private boolean foo;
private String regex;
public EndpointRefreshConfigBean(@Value("${endpoint.foo}") boolean foo,
@Value("${endpoint.regex}") String regex) {
this.foo = foo;
this.regex = regex;
}
// getters and setters
}
Next, we need to make these properties discoverable and reloadable by creating wrapper classes such as ReloadableProperties and ReloadablePropertySource.
Finally, let’s update our API handler to use an instance of EndpointRefreshConfigBean for controlling the toggle flow:
@GetMapping("/foo")
public ResponseEntity<String> fooHandler() {
if (endpointRefreshConfigBean.isFoo()) {
return ResponseEntity.status(200).body("foo");
} else {
return ResponseEntity.status(503).body("endpoint is unavailable");
}
}
4.2. Verification
First, let’s verify the /foo endpoint when the value of the endpoint.foo property is set to true:
$ curl -isXGET http://localhost:9090/foo
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 3
Date: Sat, 12 Nov 2022 15:28:52 GMT
foo
Next, let’s set the value of the endpoint.foo property to false, and check if the endpoint is still accessible:
endpoint.foo=false
We’ll notice that the /foo endpoint is still enabled. That’s because we need to reload the property sources by invoking the /refresh endpoint. So, let’s do this once:
$ curl -Is --request POST 'http://localhost:8081/actuator/refresh'
HTTP/1.1 200
Content-Type: application/vnd.spring-boot.actuator.v3+json
Transfer-Encoding: chunked
Date: Sat, 12 Nov 2022 15:34:24 GMT
Finally, let’s try to access the /foo endpoint:
$ curl -isXGET http://localhost:9090/springbootapp/foo
HTTP/1.1 503
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 23
Date: Sat, 12 Nov 2022 15:35:26 GMT
Connection: close
endpoint is unavailable
We can see that the endpoint is disabled after the refresh.
4.3. Pros and Cons
The Spring Cloud and Actuator approach has advantages and disadvantages over fetching the properties directly from the environment.
Firstly, when we rely on the /refresh endpoint, we’ve finer control than a time-based file reloading strategy. So the application isn’t making unnecessary I/O calls in the background. However, in the case of a distributed system, we need to ensure that we’re invoking the /refresh endpoint for all the nodes.
Secondly, managing a configuration bean with the @RefreshScope annotation requires us to explicitly define the member variables in EndpointRefreshConfigBean class to map with the property in the extra.properties file. So, this approach adds the overhead of making code changes in the configuration beans whenever we add or remove properties.
Finally, we must also note that a script could easily resolve the first issue, and the second issue is more specific to how we’re leveraging the properties. If we’re using a regex-based URL pattern with the Filter, then we can control multiple endpoints with a single property with no code changes to the configuration bean.
5. Conclusion
In this article, we explored multiple strategies to toggle API endpoints at runtime in a Spring Boot Application. While doing so, we leveraged some core concepts, such as hot-reloading of properties and the @RefreshScope annotation.
As always, the complete source code for the tutorial is available over on GitHub.