BAEL-5854: Toggle Endpoints at Runtime with Spring Boot (#13014)

Co-authored-by: Tapan Avasthi <tavasthi@Tapans-MacBook-Air.local>
This commit is contained in:
Tapan Avasthi 2022-11-18 07:49:32 +05:30 committed by GitHub
parent 5bfbf38f59
commit f9e34aa9f1
15 changed files with 404 additions and 0 deletions

View File

@ -55,6 +55,7 @@
<module>spring-boot-mvc-2</module>
<module>spring-boot-mvc-3</module>
<module>spring-boot-mvc-4</module>
<module>spring-boot-mvc-5</module>
<module>spring-boot-mvc-birt</module>
<module>spring-boot-mvc-jersey</module>
<module>spring-boot-nashorn</module>

View File

@ -0,0 +1,5 @@
## Spring Boot MVC
This module contains articles about Spring Web MVC in Spring Boot projects.
### Relevant Articles:

View File

@ -0,0 +1,2 @@
endpoint.foo=true
endpoint.regex=.*

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-mvc-5</artifactId>
<name>spring-boot-mvc-5</name>
<packaging>jar</packaging>
<description>Module For Spring Boot MVC Web</description>
<parent>
<groupId>com.baeldung.spring-boot-modules</groupId>
<artifactId>spring-boot-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>${start-class}</mainClass>
<layout>JAR</layout>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<spring.fox.version>3.0.0</spring.fox.version>
<start-class>com.baeldung.springboot.swagger.ArticleApplication</start-class>
<spring-cloud.version>2021.0.5</spring-cloud.version>
</properties>
</project>

View File

@ -0,0 +1,47 @@
package com.baeldung.dynamicendpoints;
import java.io.File;
import java.util.Properties;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.baeldung.dynamicendpoints.config.ReloadableProperties;
@SpringBootApplication
@EnableWebMvc
public class DynamicEndpointApp {
public static void main(String[] args) {
SpringApplication.run(DynamicEndpointApp.class, args);
}
@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;
}
@Bean
@ConditionalOnBean(PropertiesConfiguration.class)
@Primary
public Properties properties(PropertiesConfiguration propertiesConfiguration) throws Exception {
ReloadableProperties properties = new ReloadableProperties(propertiesConfiguration);
return properties;
}
}

View File

@ -0,0 +1,35 @@
package com.baeldung.dynamicendpoints.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@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;
}
public boolean isFoo() {
return foo;
}
public void setFoo(boolean foo) {
this.foo = foo;
}
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
}

View File

@ -0,0 +1,27 @@
package com.baeldung.dynamicendpoints.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@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;
}
}

View File

@ -0,0 +1,10 @@
package com.baeldung.dynamicendpoints.config;
public class PropertiesException extends RuntimeException {
public PropertiesException() {
}
public PropertiesException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,23 @@
package com.baeldung.dynamicendpoints.config;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import org.apache.commons.configuration.PropertiesConfiguration;
public class ReloadableProperties extends Properties {
private PropertiesConfiguration propertiesConfiguration;
public ReloadableProperties(PropertiesConfiguration propertiesConfiguration) throws IOException {
super.load(new FileReader(propertiesConfiguration.getFile()));
this.propertiesConfiguration = propertiesConfiguration;
}
@Override
public String getProperty(String key) {
String val = propertiesConfiguration.getString(key);
super.setProperty(key, val);
return val;
}
}

View File

@ -0,0 +1,32 @@
package com.baeldung.dynamicendpoints.config;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
public class ReloadablePropertySource extends PropertySource {
private final PropertiesConfiguration propertiesConfiguration;
public ReloadablePropertySource(String name, PropertiesConfiguration propertiesConfiguration) {
super(name);
this.propertiesConfiguration = propertiesConfiguration;
}
public ReloadablePropertySource(String name, String path) throws ConfigurationException {
super(StringUtils.hasText(name) ? path : name);
try {
this.propertiesConfiguration = new PropertiesConfiguration(path);
this.propertiesConfiguration.setReloadingStrategy(new FileChangedReloadingStrategy());
} catch (PropertiesException e) {
throw e;
}
}
@Override
public Object getProperty(String s) {
return propertiesConfiguration.getProperty(s);
}
}

View File

@ -0,0 +1,28 @@
package com.baeldung.dynamicendpoints.config;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
@Configuration
public class ReloadablePropertySourceConfig {
private ConfigurableEnvironment env;
public ReloadablePropertySourceConfig(@Autowired ConfigurableEnvironment env) {
this.env = env;
}
@Bean
@ConditionalOnProperty(name = "dynamic.endpoint.config.location", matchIfMissing = false)
public ReloadablePropertySource reloadablePropertySource(PropertiesConfiguration properties) {
ReloadablePropertySource reloadablePropertySource = new ReloadablePropertySource("toggle-endpoints", properties);
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(reloadablePropertySource);
return reloadablePropertySource;
}
}

View File

@ -0,0 +1,58 @@
package com.baeldung.dynamicendpoints.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.baeldung.dynamicendpoints.filter.DynamicEndpointFilter;
import com.baeldung.dynamicendpoints.config.EndpointRefreshConfigBean;
import com.baeldung.dynamicendpoints.config.EnvironmentConfigBean;
@RestController
@RequestMapping("/")
public class AppController {
private EndpointRefreshConfigBean endpointRefreshConfigBean;
private EnvironmentConfigBean environmentConfigBean;
@Autowired
public AppController(EndpointRefreshConfigBean endpointRefreshConfigBean, EnvironmentConfigBean environmentConfigBean) {
this.endpointRefreshConfigBean = endpointRefreshConfigBean;
this.environmentConfigBean = environmentConfigBean;
}
@GetMapping("/foo")
public ResponseEntity<String> fooHandler() {
if (endpointRefreshConfigBean.isFoo()) {
return ResponseEntity.status(200)
.body("foo");
} else {
return ResponseEntity.status(503)
.body("endpoint is unavailable");
}
}
@GetMapping("/bar1")
public String bar1Handler() {
return "bar1";
}
@GetMapping("/bar2")
public String bar2Handler() {
return "bar2";
}
@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;
}
}

View File

@ -0,0 +1,38 @@
package com.baeldung.dynamicendpoints.filter;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
public class DynamicEndpointFilter extends OncePerRequestFilter {
private Environment environment;
public DynamicEndpointFilter(Environment environment) {
this.environment = environment;
}
@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);
}
}
}

View File

@ -0,0 +1,12 @@
server.port=9090
management.server.port=8081
management.server.address=127.0.0.1
management.endpoints.web.exposure.include=*
logging.level.org.springframework=INFO
#Dynamic Endpoint
spring.main.allow-bean-definition-overriding=true
dynamic.endpoint.config.location=file:extra.properties
spring.properties.refreshDelay=1

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>