Spring Boot 在运行时启用和禁用端点
1、概览
在 Spring Boot 运行时,出于一些维护目的,我们可能需要动态地启用、禁用某些端点。
在本教程中,我们将学习如何使用 Spring Cloud、Spring Actuator 和 Apache 的 Commons Configuration 等几个常用库在运行时启用和禁用 Spring Boot 端点。
2、项目设置
以下是 Spring Boot 项目的关键设置。
2.1、Maven 依赖
首先,在 pom.xml
文件中添加 spring-boot-starter-actuator 依赖,用于暴露 /refresh
端点:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.5</version>
</dependency>
接下来,添加 spring-cloud-starter 依赖,因为稍后需要使用其 @RefreshScope
注解来重载 Environment
中的属性源:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>3.1.5</version>
</dependency>
还必须在项目 pom.xml
文件的 <dependencyManagement>
中添加 Spring Cloud 的 BOM,以便 Maven 使用兼容版本的 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>
最后,由于我们需要在运行时重新加载文件的功能,所以还要添加 commons-configuration 依赖:
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
2.2、配置
首先,在 application.properties
文件中添加配置,在应用中启用 /refresh
端点:
management.server.port=8081
management.endpoints.web.exposure.include=refresh
接下来,定义一个额外的配置源,用于重新加载属性:
dynamic.endpoint.config.location=file:extra.properties
此外,在 application.properties
文件中定义 spring.properties.refreshDelay
属性:
spring.properties.refreshDelay=1
最后,在 extra.properties
文件中添加两个属性:
endpoint.foo=false
endpoint.regex=.*
在后面的章节中,我们会了解这些属性的核心意义。
2.3、API 端点
首先,定义一个 GET
API,其路径为 /foo
:
@GetMapping("/foo")
public String fooHandler() {
return "foo";
}
接下来,再定义两个 GET
API,路径分别是 /bar1
和 /bar2
:
@GetMapping("/bar1")
public String bar1Handler() {
return "bar1";
}
@GetMapping("/bar2")
public String bar2Handler() {
return "bar2";
}
在下面的章节中,我们将学习如何切换单个端点 /foo
的状态。以及如何通过简单的 regex
(正则)切换一组端点,即 /bar1
和 /bar2
的状态。
2.4、配置 DynamicEndpointFilter
要在运行时切换一组端点的状态,可以使用 Filter
。通过使用 endpoint.regex
表达式匹配请求的端点。在匹配成功时允许请求,匹配失败时响应 503 HTTP 状态码。
定义 DynamicEndpointFilter
类,继承 OncePerRequestFilter
:
public class DynamicEndpointFilter extends OncePerRequestFilter {
private Environment environment;
// ...
}
覆写 doFilterInternal()
方法,添加表达式匹配逻辑:
@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);
}
}
注意,endpoint.regex
属性的初始值为 .*
,也就是允许所有请求。
3、通过 Environment Properties 切换状态
在本节中,我们将学习如何通过 Environment
Properties 热重载 extra.properties
文件中的配置值。
3.1、重载配置
首先使用 FileChangedReloadingStrategy
为 PropertiesConfiguration
定义一个 Bean:
@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;
}
注意,属性源是通过 application.properties
文件中的 dynamic.endpoint.config.location
属性设置的。此外,根据 spring.properties.refreshDelay
属性的定义,重新加载的延迟时间为 1 秒。
接下来,定义 EnvironmentConfigBean
,用于在运行时读取特定于端点的属性:
@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;
}
}
创建 FilterRegistrationBean
来注册 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、验证
首先,运行应用程序并访问 /bar1
或 /bar2
API:
$ 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
不出所料,返回了 200 OK
HTTP 响应,因为 endpoint.regex
属性的初始值就是启用所有端点。
接下来,更改 extra.properties
文件中的 endpoint.regex
属性,只启用 /foo
端点:
endpoint.regex=.*/foo
这一次,让我们看看能否访问 /bar1
API 端点:
$ 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"}
果然,DynamicEndpointFilter
禁用了该端点,并响应了 HTTP 503 状态码的错误响应。
最后,还可以检查一下是否能访问 /foo
API 端点:
$ 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
/foo
端点还是正常可访问的,说明配置OK。
4、通过 Spring Cloud 和 Actuator 切换状态
在本节中,我们将学习另一种方法,即使用 @RefreshScope
注解和 Actuator /refresh
端点在运行时切换 API 端点状态。
4.1、使用 @RefreshScope
配置端点
首先,需要定义用于切换端点状态的配置 Bean,并用 @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;
}
// get、set 方法省略
}
接下来,需要创建封装类(如 ReloadableProperties
和 ReloadablePropertySource
),使这些属性可被发现和重新加载。
最后,更新 API Handler,使用 EndpointRefreshConfigBean
的实例来控制切换流:
@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、验证
首先,当 endpoint.foo
属性的值设置为 true
时,验证 /foo
端点:
$ 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
接下来,将 endpoint.foo
属性的值设置为 false
,然后检查端点是否仍可访问:
endpoint.foo=false
你会注意到 /foo
端点仍处于启用状态。这是因为我们需要通过调用 /refresh
端点来重新加载属性源。
因此,先执行 /actuator/refresh
请求:
$ 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
再尝试访问 /foo
端点:
$ 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
如你所见,刷新后端点被禁用。
4.3、利弊
这 2 种实现方式各有利弊。
首先,使用 /refresh
端点时,控制粒度可以比基于时间的文件重载更精细。应用不会在后台进行额外的 I/O 调用。不过,在分布式系统中,需要确保为所有节点调用 /refresh
端点。
其次,使用 @RefreshScope
注解管理配置 Bean 需要明确定义 EndpointRefreshConfigBean
类中的成员变量,以便与 extra.properties
文件中的属性进行映射。因此,每当添加或删除属性时,这种方法都会增加修改配置 Bean 代码的开销。
最后,我想说,使用脚本可以轻松解决第一个问题,而第二个问题则与我们利用属性的方式有关。如果我们在 Filter
中使用基于正则的 URL 表达式,那么我们就可以用一个属性控制多个端点,而无需修改配置 Bean 的代码。
5、总结
在本文中,我们探索了在 Spring Boot 应用程序中运行时切换 API 端点可用状态的多种策略。在此实现中,我们利用了一些核心概念,如属性的热重载和 @RefreshScope
注解。
参考:https://www.baeldung.com/spring-boot-enable-disable-endpoints-at-runtime