自定义 Endpoint
一句话定位:Spring Boot Actuator 的
@Endpoint注解允许开发者以声明式方式创建自定义运维端点,通过@ReadOperation、@WriteOperation和@Selector定义 REST 风格的读写操作,将业务运维能力(如开关配置、数据清理)直接暴露到 Actuator 管理框架中。
定义与作用
Actuator 内置的 health、metrics、info 等端点覆盖的是通用基础设施层面。但生产环境中,运维团队经常需要执行一些业务级操作:查看当前功能开关状态、手动刷新缓存、触发对账任务、清理过期会话等。
传统做法是额外编写一个 AdminController,再单独配置访问控制。这种方式的问题是:管理接口和业务 Controller 混在一起,URL 风格不统一(有的 /admin/*,有的 /api/ops/*),安全策略需要重复配置,监控难以区分管理流量和业务流量。
Spring Boot 的自定义 Endpoint 机制允许开发者创建与 Actuator 原生端点风格完全一致的自定义接口,自动继承管理端口隔离、端点暴露控制、安全框架集成等能力。
| 对比维度 | 传统手写 AdminController | Spring Boot 自定义 @Endpoint |
|---|---|---|
| URL 规范 | 各团队自行定义,风格混乱 | 统一在 /actuator/{id} 下 |
| 端口隔离 | 需手动实现或共用业务端口 | 自动跟随 management.server.port |
| 暴露控制 | 需手写拦截器或 Security 配置 | 通过 management.endpoints.web.exposure.include 统一控制 |
| 操作语义 | 仅支持 HTTP 方法映射 | 原生支持 @ReadOperation(GET)、@WriteOperation(POST)、@DeleteOperation(DELETE) |
| 路径变量 | 使用 Spring MVC @PathVariable | 使用 @Selector,语义更清晰 |
适用位置与常用属性
自定义端点是一个标注了 @Endpoint 的 Spring Bean,其操作通过方法级别的注解定义。
注解说明
| 注解 | 适用位置 | 作用 | 映射 HTTP 方法 |
|---|---|---|---|
@Endpoint(id = "xxx") | 类 | 声明端点,id 为 URL 路径片段 | — |
@ReadOperation | 方法 | 读取操作,无副作用 | GET |
@WriteOperation | 方法 | 写入操作,可能改变状态 | POST |
@DeleteOperation | 方法 | 删除操作 | DELETE |
@Selector | 参数 | 路径变量,从 URL 中提取 | 如 /actuator/xxx/{key} |
端点 ID 约束:只能包含字母、数字和连字符,且不能是 Actuator 保留字(如
health、metrics、info、env等)。Spring Boot 2.7.x 对非法 ID 会在启动时校验失败。
配置属性
management:
server:
port: 8081
endpoints:
web:
exposure:
include: "health,info,feature-flags"
endpoint:
feature-flags:
enabled: true
核心原理
自定义端点注册与请求映射流程
流程解读:
- 开发者编写类并标注
@Endpoint,内部方法标注@ReadOperation或@WriteOperation。 - Spring Boot 启动时,
EndpointDiscoverer扫描所有@EndpointBean,提取端点 ID 和操作元数据。 WebEndpointExtension将每个操作映射为 HTTP 路由:@ReadOperation→ GET,@WriteOperation→ POST,@DeleteOperation→ DELETE。- 请求到达时,Web 适配器将 HTTP 参数和路径变量(
@Selector)绑定到方法参数,调用端点方法,并将返回值序列化为 JSON。
端点架构层次
完整示例
场景说明
飞翔科技的学生成绩管理系统需要一套**功能开关(Feature Flags)**机制,用于灰度发布。架构师白歌要求:运维人员能通过 Actuator 端点查看当前开关状态,并能远程修改某个开关的开启/关闭状态。小崔需要实现这个自定义端点,并验证其读写操作。
操作前:手写 AdminController
package com.feixiang.student.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/admin/features")
public class FeatureAdminController {
private final Map<String, Boolean> features = new ConcurrentHashMap<>();
@GetMapping
public Map<String, Boolean> list() {
return features;
}
@PostMapping("/{key}")
public void set(@PathVariable String key, @RequestParam boolean enabled) {
features.put(key, enabled);
}
}
问题:
- URL
/admin/features与业务 Controller 混在一起,需额外配置 Spring Security 保护。 - 管理流量占用业务端口 8080,缺乏隔离。
- 接口风格与 Actuator 不一致,监控系统需要单独适配。
使用该特性的完整代码
小崔改用 Actuator 自定义端点实现:
package com.feixiang.student.endpoint;
import org.springframework.boot.actuate.endpoint.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 功能开关自定义端点
* 提供当前系统灰度功能开关的查询和修改能力
*
* @author 小崔
* @since 2024
*/
@Component
@Endpoint(id = "feature-flags")
public class FeatureFlagsEndpoint {
private final Map<String, Boolean> features = new ConcurrentHashMap<>();
public FeatureFlagsEndpoint() {
// 初始化默认开关状态
features.put("new-score-ui", false);
features.put("batch-import", true);
features.put("sms-notification", false);
}
/**
* 读取操作:获取所有功能开关当前状态
* HTTP: GET /actuator/feature-flags
*/
@ReadOperation
public Map<String, Boolean> featureFlags() {
return Map.copyOf(features);
}
/**
* 读取操作:获取单个功能开关状态
* HTTP: GET /actuator/feature-flags/{featureName}
*/
@ReadOperation
public Boolean featureFlag(@Selector String featureName) {
return features.get(featureName);
}
/**
* 写入操作:修改功能开关状态
* HTTP: POST /actuator/feature-flags/{featureName}
* 请求体:{ "enabled": true }
*/
@WriteOperation
public void setFeatureFlag(@Selector String featureName, boolean enabled) {
features.put(featureName, enabled);
}
}
application.yml 配置:
spring:
application:
name: student-app
server:
port: 8080
management:
server:
port: 8081
endpoints:
web:
exposure:
include: "health,info,feature-flags"
endpoint:
feature-flags:
enabled: true
注意:自定义端点类无需继承任何父类或实现任何接口,仅通过
@Endpoint和操作方法注解即可完整定义。
操作后运行结果及分析
查询所有开关:
$ curl http://localhost:8081/actuator/feature-flags
返回结果:
{
"new-score-ui": false,
"batch-import": true,
"sms-notification": false
}
查询单个开关:
$ curl http://localhost:8081/actuator/feature-flags/batch-import
返回结果:
true
修改开关状态:
$ curl -X POST http://localhost:8081/actuator/feature-flags/new-score-ui \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
再次查询:
$ curl http://localhost:8081/actuator/feature-flags/new-score-ui
返回结果:
true
分析:
@Endpoint(id = "feature-flags")生效:端点自动挂载到/actuator/feature-flags,与原生端点风格统一。@ReadOperation生效:支持无参查询全部,也支持@Selector查询单个,返回类型自动序列化为 JSON。@WriteOperation生效:HTTP POST 请求自动映射到写入方法,请求体中的 JSON 字段按名称绑定到方法参数。- 管理端口隔离生效:所有操作都在 8081 端口完成,与业务端口 8080 完全分离。
- 暴露控制生效:仅因
include列表中显式包含feature-flags,该端点才对外可见。生产环境可通过调整include快速关闭。
易错场景与面试考点
易错场景一:端点 ID 与保留字冲突
// 错误示范:使用了 Actuator 保留字
@Component
@Endpoint(id = "health")
public class CustomHealthEndpoint {
...
}
后果:应用启动时抛出 IllegalArgumentException: Endpoint ID 'health' is already in use or is reserved。端点注册失败,应用启动中断。
正确做法:自定义端点 ID 避免使用 health、metrics、info、env、beans、conditions、configprops、mappings、loggers、threaddump、heapdump、shutdown、scheduledtasks 等保留字。建议采用带业务前缀的命名,如 feature-flags、cache-stats、job-status。
易错场景二:端点 ID 包含非法字符
// 错误示范:端点 ID 包含下划线
@Component
@Endpoint(id = "feature_flags")
public class FeatureFlagsEndpoint {
...
}
后果:Spring Boot 2.7.x 对端点 ID 的合法性校验更严格,包含下划线会触发警告或启动失败(取决于配置)。
正确做法:端点 ID 只能使用小写字母、数字和连字符。正确写法为 feature-flags。
易错场景三:未暴露端点导致 404
# 错误示范:include 列表中遗漏了自定义端点
management:
endpoints:
web:
exposure:
include: "health,info"
后果:即使端点 Bean 正确定义,访问 /actuator/feature-flags 仍返回 404。小崔误以为端点代码有问题。
正确做法:显式将自定义端点 ID 加入暴露列表:
management:
endpoints:
web:
exposure:
include: "health,info,feature-flags"
面试考点
Q:@Endpoint 和 @Controller 有什么区别?
@Endpoint是 Actuator 框架的专用注解,创建的端点自动挂载到/actuator/{id}路径下,继承管理端口隔离、暴露控制、安全策略等原生能力。@Controller是 Spring MVC 的 Web 层注解,需要手动配置 URL 前缀、端口隔离和安全策略,且风格与 Actuator 不一致。
Q:@ReadOperation 和 @WriteOperation 分别对应什么 HTTP 方法?
@ReadOperation映射 HTTP GET,用于无副作用的查询;@WriteOperation映射 HTTP POST,用于改变状态或触发操作;@DeleteOperation映射 HTTP DELETE。这种语义映射是固定的,不能通过额外配置修改。
Q:@Selector 和 Spring MVC 的 @PathVariable 有什么区别?
两者在用法上类似,都是提取 URL 路径变量。但
@Selector是 Actuator 端点框架的专属注解,语义上表示"从端点的资源集合中选择一项",与@Endpoint配套使用。在自定义端点中必须使用@Selector,不能使用@PathVariable。
Q:自定义端点的方法返回值可以是什么类型?
可以是任意 POJO、Map、String、Boolean 等。Actuator 会自动将其序列化为 JSON(或 XML,取决于请求 Accept 头)。返回
void时,HTTP 响应体为空。若返回null,@ReadOperation会返回 404 Not Found(表示资源不存在),这是 Actuator 的默认语义约定。