1. 概述
Spring Boot应用程序中的端点是与之交互的机制。有时,例如在计划外维护窗口期间,我们可能希望暂时限制应用程序与外部的交互。
在本教程中,我们将学习使用一些流行的库(例如Spring Cloud、Spring Actuator和Apache的Commons Configuration)在Spring Boot应用程序中在运行时启用和禁用端点。
2. 设置
在本节中,让我们专注于为我们的Spring Boot项目设置关键方面。
2.1 Maven依赖项
首先,我们需要Spring Boot应用程序公开/refresh端点,所以让我们在项目的pom.xml文件中添加spring-boot-starter-actuator依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.7.5</version>
</dependency>
接下来,由于稍后我们需要@RefreshScope注解来重新加载环境中的属性源,因此让我们添加spring-cloud-starter依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>3.1.5</version>
</dependency>
此外,我们还必须在项目的pom.xml文件的依赖管理部分添加Spring Cloud的,以便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端点
首先,让我们在/foo路径中定义一个可用的示例GET API:
@GetMapping("/foo")
public String fooHandler() {
return "foo";
}
接下来,让我们分别在/bar1和/bar2路径上定义另外两个可用的GET API:
@GetMapping("/bar1")
public String bar1Handler() {
return "bar1";
}
@GetMapping("/bar2")
public String bar2Handler() {
return "bar2";
}
在以下部分中,我们将学习如何切换单个端点,例如/foo。此外,我们还将看到如何切换一组端点,即/bar1和/bar2,可通过简单的正则表达式识别。
2.4 配置DynamicEndpointFilter
要在运行时切换一组端点,我们可以使用Filter。通过使用endpoint.regex模式匹配请求的端点,我们可以允许它成功或发送503 HTTP响应状态以表示匹配不成功。
因此,让我们通过扩展OncePerRequestFilter来定义DynamicEndpointFilter类:
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. 切换使用环境属性
在本节中,我们将学习如何从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
完美的!看起来我们做对了。
4. 切换使用Spring Cloud和Actuator
在本节中,我们将学习另一种方法,即使用@RefreshScope注解和执行器/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;
}
// getters and setters
}
接下来,我们需要通过创建包装类(例如ReloadableProperties和ReloadablePropertySource)使这些属性可发现和可重新加载。
最后,让我们更新我们的API处理程序以使用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端点来重新加载属性源。所以,让我们做一次:
$ 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 优点和缺点
与直接从环境中获取属性相比,Spring Cloud和Actuator方法有优点也有缺点。
首先,当我们依赖/refresh端点时,我们比基于时间的文件重新加载策略有更好的控制。所以应用程序不会在后台进行不必要的I/O调用。但是,在分布式系统的情况下,我们需要确保我们正在为所有节点调用/refresh端点。
其次,使用@RefreshScope注解管理配置bean需要我们在EndpointRefreshConfigBean类中显式定义成员变量以映射到extra.properties文件中的属性。因此,这种方法增加了在我们添加或删除属性时在配置bean中更改代码的开销。
最后,我们还必须注意脚本可以轻松解决第一个问题,而第二个问题更具体到我们如何利用这些属性。如果我们将基于正则表达式的URL模式与过滤器一起使用,那么我们可以使用单个属性控制多个端点,而无需更改配置bean的代码。
5. 总结
在本文中,我们探讨了在Spring Boot应用程序运行时切换API端点的多种策略。在这样做的同时,我们利用了一些核心概念,例如属性的热重载和@RefreshScope注解。
与往常一样,本教程的完整源代码可在GitHub上获得。