1. 概述
Spring Boot应用中的接口是与外部交互的核心机制。在突发维护等场景下,我们可能需要临时限制应用的外部交互。
本教程将介绍如何使用Spring Cloud、Spring Actuator和Apache Commons Configuration这三个主流库,在Spring Boot应用中实现运行时动态启用/禁用接口。
2. 项目准备
本节重点配置Spring Boot项目的关键组件。
2.1 Maven依赖
首先需要暴露/refresh
接口,在pom.xml
中添加spring-boot-starter-actuator
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>3.1.5</version>
</dependency>
后续需要使用@RefreshScope
注解重新加载环境属性,添加spring-cloud-starter
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>3.1.5</version>
</dependency>
⚠️ 必须在dependencyManagement
中添加Spring Cloud的BOM,确保版本兼容:
<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
设置属性重载延迟时间:
spring.properties.refreshDelay=1
在extra.properties
中添加两个关键属性:
endpoint.foo=false
endpoint.regex=.*
这些属性在后续章节会发挥重要作用。
2.3 API接口定义
创建三个示例接口:
@GetMapping("/foo")
public String fooHandler() {
return "foo";
}
@GetMapping("/bar1")
public String bar1Handler() {
return "bar1";
}
@GetMapping("/bar2")
public String bar2Handler() {
return "bar2";
}
后续我们将实现:
- 单独控制
/foo
接口 - 通过正则表达式批量控制
/bar1
和/bar2
接口
2.4 配置DynamicEndpointFilter
通过过滤器实现接口批量控制,继承OncePerRequestFilter
:
public class DynamicEndpointFilter extends OncePerRequestFilter {
private Environment environment;
// 构造函数注入
public DynamicEndpointFilter(Environment environment) {
this.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 配置重载机制
定义PropertiesConfiguration
Bean,使用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;
}
📌 关键点:
- 属性源路径由
dynamic.endpoint.config.location
指定 - 重载延迟1秒由
spring.properties.refreshDelay
控制
创建属性访问Bean:
@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;
}
}
注册过滤器:
@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
:
$ 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
✅ 初始配置允许所有接口访问。
修改extra.properties
仅允许/foo
:
endpoint.regex=.*/foo
再次访问/bar1
:
$ 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"}
❌ 过滤器成功拦截并返回503状态码。
验证/foo
接口:
$ 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
接口正常访问。
4. 基于Spring Cloud和Actuator的方案
本节介绍使用@RefreshScope
和/refresh
接口的替代方案。
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
包装类(实现略)。
更新接口控制器使用配置Bean:
@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
修改extra.properties
:
endpoint.foo=false
❌ 此时接口仍可访问,需要手动触发刷新:
$ 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 优缺点分析
优势:
- 精确控制:相比定时文件扫描,
/refresh
接口提供更精准的刷新时机 - 减少I/O:避免后台不必要的文件轮询
劣势:
- 分布式问题:需确保所有节点都调用
/refresh
接口 - 代码耦合:新增属性需修改
EndpointRefreshConfigBean
类 - 维护成本:属性变更需要代码同步更新
💡 建议:如果使用正则表达式批量控制接口,单属性即可管理多个接口,无需频繁修改配置类。
5. 总结
本文探讨了在Spring Boot应用中运行时动态控制接口的三种实现方案:
- 环境属性+文件监听:简单粗暴,适合单机应用
- Spring Cloud+Actuator:企业级方案,适合分布式系统
- 过滤器+正则匹配:灵活控制接口组
核心涉及属性热重载和@RefreshScope
两大技术点。完整代码可在GitHub获取。
🔥 踩坑提示:生产环境使用Actuator时,务必做好接口安全防护!