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
}

📌 需要创建ReloadablePropertiesReloadablePropertySource包装类(实现略)。

更新接口控制器使用配置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 优缺点分析

优势:

  1. 精确控制:相比定时文件扫描,/refresh接口提供更精准的刷新时机
  2. 减少I/O:避免后台不必要的文件轮询

劣势:

  1. 分布式问题:需确保所有节点都调用/refresh接口
  2. 代码耦合:新增属性需修改EndpointRefreshConfigBean
  3. 维护成本:属性变更需要代码同步更新

💡 建议:如果使用正则表达式批量控制接口,单属性即可管理多个接口,无需频繁修改配置类。

5. 总结

本文探讨了在Spring Boot应用中运行时动态控制接口的三种实现方案

  1. 环境属性+文件监听:简单粗暴,适合单机应用
  2. Spring Cloud+Actuator:企业级方案,适合分布式系统
  3. 过滤器+正则匹配:灵活控制接口组

核心涉及属性热重载@RefreshScope两大技术点。完整代码可在GitHub获取。

🔥 踩坑提示:生产环境使用Actuator时,务必做好接口安全防护!


原始标题:Enable and Disable Endpoints at Runtime With Spring Boot