上一篇我们拆解了代理端的数据转发机制——采集到的数据经过 Controller → Client Service → Server Service 三层架构,最终通过 HTTP 或 WebSocket 送达服务端。数据到了服务端之后,第一个迎接它们的就是 Controller 层。本篇聚焦服务端的 Controller 层设计,看看它是如何统一接收各类监控数据、响应 UI 端主动探测请求、处理配置与命令下发的,以及加解密管道和 AOP 监听器回调机制是如何在框架层透明运作的。
一、Controller 层的全景图
服务端的 Controller 位于 phoenix-server 模块的 business.server.controller 包下,按职责分为三大类:
| 类别 | Controller | 职责 |
|---|---|---|
| 数据接收 | HeartbeatController、ServerController、JvmController、AlarmController、DockerController、ExceptionController、NetworkDeviceController、OfflineController | 接收代理端/客户端上报的各类监控数据包 |
| 主动探测 | HttpController、TcpController、NetworkController、DbController、DbInfo4RedisServiceController、DbInfo4MongoServiceController、DbSession4OracleController、DbSession4MysqlController、DbTableSpace4OracleController | 响应 UI 端发起的连通性测试、数据库会话/表空间查询请求 |
| 配置与命令 | MonitoringPropertiesConfigController、CommandController、InstanceController | 配置刷新、命令下发、线程池动态配置 |
三类 Controller 虽然职责不同,但共享同一套基础设施:加密管道(RequestBodyAdvice / ResponseBodyAdvice)、响应构造器(ServerPackageConstructor)、以及统一的计时与慢处理告警。下面逐一深入分析。
二、数据接收类 Controller:专用数据包的接收范式
2.1 统一的四步范式
数据接收类 Controller 负责接收代理端或客户端上报的各类监控数据包。以 HeartbeatController 为代表:
@Deprecated
@Slf4j
@RestController
@RequestMapping("/heartbeat")
@Tag(name = "信息包.心跳包")
public class HeartbeatController {
@Autowired
private ServerPackageConstructor serverPackageConstructor;
@Autowired
private IHeartbeatService heartbeatService;
@Operation(description = "接收和响应监控代理端程序或者监控客户端程序发的心跳包", summary = "接收心跳包",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
content = @Content(schema = @Schema(implementation = CiphertextPackage.class))),
responses = @ApiResponse(content = {@Content(schema = @Schema(implementation = CiphertextPackage.class))}))
@PostMapping("/accept-heartbeat-package")
public BaseResponsePackage acceptHeartbeatPackage(@RequestBody HeartbeatPackage heartbeatPackage) throws NetException {
// 计时器
TimeInterval timer = DateUtil.timer();
// 调用 Service 处理业务
Result result = this.heartbeatService.dealHeartbeatPackage(heartbeatPackage);
// 构造响应包
BaseResponsePackage baseResponsePackage = this.serverPackageConstructor.structureBaseResponsePackage(result);
// 慢处理告警
String betweenDay = timer.intervalPretty();
if (timer.intervalSecond() > 1) {
log.warn("处理心跳包耗时:{}", betweenDay);
}
return baseResponsePackage;
}
}
每个方法体内都包含相同的四个步骤:
① TimeInterval timer = DateUtil.timer(); // 开启计时
② Result result = xxxService.dealXxxPackage(pkg); // 委托 Service 处理
③ responsePackage = serverPackageConstructor // 构造响应
.structureBaseResponsePackage(result);
④ if (timer.intervalSecond() > 1) { log.warn(...); } // 慢处理日志
Controller 只做调度,不碰业务。 每个 acceptXxxPackage 方法体不超过 10 行,唯一的业务逻辑就是调用 Service 的 dealXxxPackage 方法。数据校验、持久化、告警判断——全部下沉到 Service 层。
统一的慢处理监控。 所有 Controller 方法都以 TimeInterval 计时,超过 1 秒就打印 WARN 日志。这是一个轻量但实用的性能护栏——在生产环境中,如果某个数据包处理突然变慢,运维人员可以通过这条日志快速定位到具体的 Controller 和数据类型。
2.2 强类型数据包模型
数据接收类 Controller 使用专用的 Package 类型作为 @RequestBody 参数,每种数据类型都有对应的强类型 DTO:
| Controller | @RequestBody 类型 | 携带的数据 |
|---|---|---|
| HeartbeatController | HeartbeatPackage | 心跳信息、实例状态 |
| ServerController | ServerPackage | CPU、内存、磁盘、负载等服务器指标 |
| JvmController | JvmPackage | JVM 内存、线程、GC、类加载信息 |
| AlarmController | AlarmPackage | 告警级别、告警原因、告警消息 |
| DockerController | DockerPackage | 容器列表、镜像列表、Stats 数据 |
| ExceptionController | ExceptionPackage | 异常名称、堆栈、请求参数 |
| NetworkDeviceController | NetworkDevicePackage | SNMP 采集的网络设备信息 |
| OfflineController | OfflinePackage | 实例下线通知 |
强类型的好处是显而易见的:编译期类型检查、IDE 自动补全、清晰的接口契约。调用方和接收方对数据结构有完全一致的理解,不会出现字段拼写错误或类型不匹配的问题。
三、主动探测类 Controller:通用请求 + extraMsg 扩展
3.1 设计动机
数据接收类 Controller 是「被动接收」——代理端按固定频率推送数据过来。但监控系统还有另一类需求:UI 端主动发起探测请求。比如用户在页面上点击「测试数据库连通性」,需要实时得到结果。
这类请求的特点是:参数结构多样、一次性调用、需要即时响应。为每种探测都定义一个专用 DTO 显然不现实——数据库探测需要 url/username/password,HTTP 探测需要 method/url/contentType,TCP 探测需要 hostname/port。Phoenix 的解决方案是:用通用的 BaseRequestPackage + extraMsg(JSONObject)承载业务参数。
3.2 连通性探测系列
BaseRequestPackage 的结构非常简洁:
public class BaseRequestPackage extends AbstractSuperPackage {
protected String id;
protected Date dateTime;
protected JSONObject extraMsg; // 业务参数全在这里
}
以 DbController 为例,展示参数的提取方式:
@Slf4j
@RestController
@RequestMapping("/db")
@Tag(name = "数据库信息")
public class DbController {
@Autowired
private ServerPackageConstructor serverPackageConstructor;
@Autowired
private IDbService dbService;
@PostMapping("/test-monitor-db")
public BaseResponsePackage testMonitorDb(@RequestBody BaseRequestPackage baseRequestPackage) throws NetException {
TimeInterval timer = DateUtil.timer();
JSONObject extraMsg = baseRequestPackage.getExtraMsg();
String url = extraMsg.getString("url");
String dbType = extraMsg.getString("dbType");
String username = extraMsg.getString("username");
String password = extraMsg.getString("password");
Boolean isConnected = this.dbService.testMonitorDb(url, dbType, username, password);
BaseResponsePackage baseResponsePackage = this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(true).msg(String.valueOf(isConnected)).build());
if (timer.intervalSecond() > 1) {
log.warn("测试数据库连通性耗时:{}", timer.intervalPretty());
}
return baseResponsePackage;
}
}
同样的模式在 HttpController、TcpController、NetworkController、NetworkDeviceController.testMonitorNetworkDevice() 中反复出现,区别仅在于 extraMsg 中提取的参数不同:
| Controller | 接口路径 | extraMsg 参数 |
|---|---|---|
| DbController | /test-monitor-db | url, dbType, username, password |
| HttpController | /test-monitor-http | method, urlTarget, contentType, headerParameter, bodyParameter |
| TcpController | /test-monitor-tcp | hostnameTarget, portTarget |
| NetworkController | /test-monitor-network | ipTarget |
| NetworkDeviceController | /test-monitor-network-device | ipTarget, cpPort, cpName, cpCommunity, oid, cpVersion |
| NetworkController | /get-source-ip | (无参数) |
可以看到,参数种类从 1 个到 6 个不等。这正是 extraMsg 灵活性的价值——不用为每种探测定义一个新的 DTO。
3.3 数据库运维操作系列
除了连通性探测,还有一组针对数据库深度运维的 Controller:
@Slf4j
@RestController
@Tag(name = "数据库会话.Oracle")
@RequestMapping("/db-session4oracle")
public class DbSession4OracleController {
@PostMapping("/get-session-list")
public BaseResponsePackage getSessionList(@RequestBody BaseRequestPackage baseRequestPackage) throws SQLException {
JSONObject extraMsg = baseRequestPackage.getExtraMsg();
String url = extraMsg.getString("url");
String username = extraMsg.getString("username");
String password = extraMsg.getString("password");
List<Entity> entities = this.dbSession4OracleService.getSessionList(url, username, password);
String jsonString = JSON.toJSONString(entities);
return this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(true).msg(jsonString).build());
}
@PostMapping("/destroy-session")
public BaseResponsePackage destroySession(@RequestBody BaseRequestPackage baseRequestPackage) throws SQLException {
JSONObject extraMsg = baseRequestPackage.getExtraMsg();
String url = extraMsg.getString("url");
String username = extraMsg.getString("username");
String password = extraMsg.getString("password");
List<Long> sids = extraMsg.getObject("sids", new TypeReference<List<Long>>() {});
List<Long> serials = extraMsg.getObject("serials", new TypeReference<List<Long>>() {});
this.dbSession4OracleService.destroySession(url, username, password, sids, serials);
return this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(true).msg(ResultMsgConstants.SUCCESS).build());
}
}
这组 Controller 的特点在于:不仅是「查询」,还有「操作」。 destroySession 是一个写操作——结束 Oracle 数据库会话。它需要 SID 和 Serial# 两个列表参数,通过 fastjson 的 TypeReference 实现泛型集合的反序列化。
同样,DbTableSpace4OracleController 提供了两个查询接口(按文件/按表空间),DbInfo4RedisServiceController 和 DbInfo4MongoServiceController 分别查询 Redis 和 MongoDB 的运行信息。这些 Controller 按数据库类型拆分,而不是合并为一个「通用数据库 Controller」——因为每种数据库的连接方式、查询语句、返回结构完全不同,强行统一反而增加复杂度。
3.4 数据接收与主动探测的响应差异
两类 Controller 在响应构造上有一个微妙但重要的差异:
数据接收类的 Service 返回 Result,Controller 用 serverPackageConstructor.structureBaseResponsePackage(result) 包装。Service 负责业务判断,Result.isSuccess 可能为 true 也可能为 false。
主动探测类的 Controller 直接构造 Result,isSuccess 固定为 true(因为到达 Controller 本身就说明请求成功了),而探测的实际结果放在 msg 字段中——比如 String.valueOf(isConnected) 或 jsonString。这是一个设计权衡:探测结果本质上是「数据」而非「成功/失败」,用 msg 承载可以避免引入额外的 DTO。
四、配置与命令类 Controller:从 UI 到端的控制链路
4.1 MonitoringPropertiesConfigController:配置热刷新
@Slf4j
@RestController
@RequestMapping("/monitoring-properties-config")
@Tag(name = "监控属性配置")
public class MonitoringPropertiesConfigController {
@Autowired
private MonitoringConfigPropertiesLoader monitoringConfigPropertiesLoader;
@Autowired
private ServerPackageConstructor serverPackageConstructor;
@PostMapping("/refresh")
public BaseResponsePackage refresh() {
TimeInterval timer = DateUtil.timer();
this.monitoringConfigPropertiesLoader.wakeUpMonitoringConfigPropertiesLoader();
BaseResponsePackage baseResponsePackage = this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(true).msg(ResultMsgConstants.SUCCESS).build());
if (timer.intervalSecond() > 1) {
log.warn("刷新监控配置属性耗时:{}", timer.intervalPretty());
}
return baseResponsePackage;
}
}
这个 Controller 的特点是无请求参数——连 @RequestBody 都没有。它唯一的职责是触发 MonitoringConfigPropertiesLoader 重新从数据库加载监控配置。
来看 MonitoringConfigPropertiesLoader 的内部机制:
@Component
public class MonitoringConfigPropertiesLoader {
private static MonitoringProperties monitoringProperties;
@PostConstruct
public void init() {
MonitoringProperties properties = this.loadAllMonitorConfig();
this.setMonitoringProperties(properties);
}
@Scheduled(initialDelay = 300000, fixedDelay = 300000)
public void wakeUpMonitoringConfigPropertiesLoader() {
this.setMonitoringProperties(this.loadAllMonitorConfig());
}
public MonitoringProperties loadAllMonitorConfig() {
MonitorConfig monitorConfig = this.configService.getOne(new LambdaQueryWrapper<>());
if (monitorConfig == null) {
return this.setDefaultMonitorConfig();
}
String value = monitorConfig.getValue();
return JSON.parseObject(value, MonitoringProperties.class, ...);
}
}
设计上有三个亮点:
- 双重加载机制:
@PostConstruct在启动时加载,@Scheduled每 5 分钟自动刷新。/refresh接口提供了「手动立即刷新」的能力,补充了自动定时刷新之间的空档 - 静态变量 + synchronized:配置存储在
static变量中,全局任何位置都能通过类直接访问;setMonitoringProperties加了synchronized保证并发安全 - 数据库驱动的配置中心:所有监控参数(告警阈值、告警渠道、监控开关等)存储在数据库的 JSON 字段中,修改数据库后调用
/refresh即可生效,无需重启服务
4.2 CommandController:命令下发中转站
CommandController 是服务端 Controller 中最特殊的一个——它不是接收数据,也不是响应探测,而是接收来自 UI 端的命令,并转发给代理端执行:
@Slf4j
@Tag(name = "信息包.命令信息包")
@RestController
@RequestMapping("/command")
public class CommandController {
@Autowired
private ICommandService commandService;
@PostMapping("/accept-command-package")
public BaseResponsePackage acceptCommandPackage(@RequestBody CommandPackage commandPackage) throws IOException {
TimeInterval timer = DateUtil.timer();
BaseResponsePackage baseResponsePackage = this.commandService.dealCommandPackage(commandPackage);
if (timer.intervalSecond() > 1) {
log.warn("处理命令信息包耗时:{}", timer.intervalPretty());
}
return baseResponsePackage;
}
}
注意这里有一个关键差异:Service 直接返回 BaseResponsePackage,而不是 Result。 因为命令需要通过 WebSocket 转发给代理端执行,Service 内部根据转发结果(成功/失败)自行构造响应包返回给 UI 端。
来看 CommandServiceImpl 的具体实现:
@Service
public class CommandServiceImpl implements ICommandService {
@Autowired
private ServerPackageConstructor serverPackageConstructor;
@Autowired
private IDockerService dockerService;
@Autowired
private MonitoringFrameHandler monitoringFrameHandler;
@Override
public BaseResponsePackage dealCommandPackage(CommandPackage commandPackage) {
BaseResponsePackage baseResponsePackage = null;
Command command = commandPackage.getCommand();
MonitorTypeEnums monitorTypeEnum = command.getMonitorTypeEnum();
String commandTarget = command.getCommandTarget();
if (MonitorTypeEnums.DOCKER.equals(monitorTypeEnum)) {
MonitorDocker monitorDocker = this.dockerService.getById(Long.valueOf(commandTarget));
String agentAddr = monitorDocker.getAgentCommClientId();
try {
WebSocketPackage requestPackage = new WebSocketPackage();
requestPackage.setClassName(CommandPackage.class.getName());
requestPackage.setPayload(commandPackage);
// 同步发送到代理端,超时 10 秒
this.monitoringFrameHandler.sendMsgToClientSync(agentAddr, requestPackage, 10, TimeUnit.SECONDS);
baseResponsePackage = this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(true).msg(ResultMsgConstants.SUCCESS).build());
} catch (Exception e) {
baseResponsePackage = this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(false).msg(e.getMessage()).build());
}
}
return baseResponsePackage;
}
}
命令下发的完整链路:
UI端 发送 CommandPackage(监控类型 + 命令目标 + 命令动作)
│
▼
CommandController.acceptCommandPackage()
│
▼
CommandServiceImpl.dealCommandPackage()
│ 1. 根据 monitorTypeEnum 判断命令类型(如 Docker)
│ 2. 根据 commandTarget 查询数据库,获取目标代理端的 WebSocket 客户端 ID
│ 3. 封装为 WebSocketPackage,通过 MonitoringFrameHandler 同步发送
│ 4. 超时 10 秒等待代理端响应
▼
代理端 CommandIssuingController 接收并执行命令
Command 实体包含了完整的命令描述:
public class Command extends AbstractSuperBean {
private MonitorTypeEnums monitorTypeEnum; // 监控类型(Docker等)
private String commandType; // 命令类型
private String commandAction; // 命令动作
private String commandTarget; // 命令目标(如 Docker 记录 ID)
private String commandValue; // 命令值
}
这里有一个值得关注的设计:monitorTypeEnum 作为分发依据,为将来支持更多命令类型(比如 JVM 操作、服务器操作等)预留了扩展空间。当前只实现了 DOCKER 分支,新增类型只需添加 else if 分支即可。
4.3 InstanceController:线程池动态配置
@Slf4j
@RestController
@RequestMapping("/instance")
@Tag(name = "应用程序")
public class InstanceController {
@Autowired
private IJavaThreadPoolService javaThreadPoolService;
@Autowired
private ServerPackageConstructor serverPackageConstructor;
@PostMapping("/set-instance-java-thread-pool")
public BaseResponsePackage setInstanceJavaThreadPool(@RequestBody BaseRequestPackage baseRequestPackage) {
TimeInterval timer = DateUtil.timer();
JSONObject extraMsg = baseRequestPackage.getExtraMsg();
JavaThreadPool.ThreadPoolInfoDomain threadPoolInfo =
extraMsg.getObject("threadPoolInfo", JavaThreadPool.ThreadPoolInfoDomain.class);
String endpoint = extraMsg.getString("endpoint");
String instanceId = extraMsg.getString("instanceId");
Boolean success = this.javaThreadPoolService.setInstanceJavaThreadPool(endpoint, instanceId, threadPoolInfo);
BaseResponsePackage baseResponsePackage = this.serverPackageConstructor
.structureBaseResponsePackage(Result.builder().isSuccess(true).msg(String.valueOf(success)).build());
if (timer.intervalSecond() > 1) {
log.warn("配置Java线程池耗时:{}", timer.intervalPretty());
}
return baseResponsePackage;
}
}
InstanceController 融合了「通用请求模型」和「命令下发」两种模式。从请求模型看,它使用 BaseRequestPackage + extraMsg,与主动探测类 Controller 一致;但从行为上看,它不是查询而是远程配置下发。
来看 Service 层的实现:
@Override
public Boolean setInstanceJavaThreadPool(String endpoint, String instanceId,
JavaThreadPool.ThreadPoolInfoDomain threadPoolInfo) {
try {
JavaThreadPool javaThreadPool = JavaThreadPool.builder()
.threadPoolInfoDomains(Lists.newArrayList(threadPoolInfo)).build();
JavaThreadPoolPackage javaThreadPoolPackage =
this.serverPackageConstructor.structureJavaThreadPoolPackage(javaThreadPool);
WebSocketPackage requestPackage = new WebSocketPackage();
requestPackage.setClassName(JavaThreadPoolPackage.class.getName());
requestPackage.setPayload(javaThreadPoolPackage);
String websocketClientId = WebsocketClientIdGenerator.generate(endpoint, instanceId);
this.monitoringFrameHandler.sendMsgToClientSync(websocketClientId, requestPackage, 10, TimeUnit.SECONDS);
return true;
} catch (Exception e) {
return false;
}
}
与 CommandServiceImpl 的模式完全一致:构造 WebSocketPackage → 通过 MonitoringFrameHandler.sendMsgToClientSync 同步发送 → 10 秒超时。区别在于:CommandServiceImpl 通过数据库查询代理端地址,而 InstanceController 通过 WebsocketClientIdGenerator.generate(endpoint, instanceId) 直接根据端点和实例 ID 生成 WebSocket 客户端标识。
五、三类 Controller 的横向对比
将三类 Controller 的设计放在一起对比:
| 维度 | 数据接收 | 主动探测 | 配置与命令 |
|---|---|---|---|
| 请求发起方 | 代理端/客户端 | UI 端 | UI 端 |
| 请求模型 | 专用 Package(强类型) | BaseRequestPackage + extraMsg | BaseRequestPackage + extraMsg 或专用 Package |
| 响应构造 | Service 返回 Result | Controller 构造 Result | 混合:CommandController 由 Service 返回 BaseResponsePackage;其余由 Controller 构造 Result |
| 数据流向 | 上行(客户端→服务端) | 下行(服务端→外部资源) | 下行+转发(UI→服务端→代理端) |
| AOP 切面 | 有(监听器回调) | 无 | 无 |
| @Deprecated | 多数已标记 | 未标记 | 未标记 |
三个值得注意的设计选择:
第一,响应构造方式不同。 数据接收类和主动探测类由 Controller 调用 serverPackageConstructor.structureBaseResponsePackage(result) 完成响应包装;命令类中 CommandController 的 Service 直接返回 BaseResponsePackage,因为 Service 需要根据 WebSocket 转发结果构造成功/失败响应;而 InstanceController 和 MonitoringPropertiesConfigController 仍然由 Controller 构造响应。
第二,只有数据接收类配套了 AOP 切面。 主动探测和命令类不需要监听器回调——探测是即时操作,命令是点对点转发,都不需要「数据到达后触发附加逻辑」这种事件驱动模式。
第三,数据接收类正在向 WebSocket 迁移。 多数数据接收 Controller 标注了 @Deprecated,而主动探测和命令类没有——因为后两者的交互模式天然适合 HTTP 请求-响应(UI 操作需要即时反馈),而数据上报更适合长连接推送。
六、加密管道:RequestBodyAdvice 与 ResponseBodyAdvice
三类 Controller 虽然职责不同,但共享同一套加密管道。Swagger 注解中请求体类型统一标注为 CiphertextPackage,而 Controller 方法参数却是明文对象——这之间的转换由 Spring MVC 的通知机制完成。
6.1 请求解密:RequestPackageDecryptAdvice
@RestControllerAdvice(basePackages = "com.gitee.pifeng.monitoring.server.business.server.controller")
public class RequestPackageDecryptAdvice implements RequestBodyAdvice {
@Override
public boolean supports(...) {
return true; // 总开关:对所有请求生效
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, ...) throws IOException {
return new HttpInputMessagePackageDecrypt(inputMessage);
}
}
basePackages 精确限定在 Controller 包内,不影响服务端其他模块。核心逻辑在 HttpInputMessagePackageDecrypt.getBody() 中:
public InputStream getBody() throws DecryptionException {
// 1. 读取原始请求体
String bodyStr = IOUtils.toString(this.originalBody, StandardCharsets.UTF_8);
// 2. 编码转换后反序列化为密文数据包
JsonStringEncoder encoder = JsonStringEncoder.getInstance();
byte[] fb = encoder.encodeAsUTF8(bodyStr);
CiphertextPackage ciphertextPackage = OBJECT_MAPPER.readValue(fb, CiphertextPackage.class);
// 3. 解密(含可选解压)
String decryptStr = MsgPayloadUtils.decryptPayloadFrom(ciphertextPackage);
// 4. 返回明文输入流,交给 Spring MVC 反序列化
return IOUtils.toInputStream(decryptStr, StandardCharsets.UTF_8);
}
解密位于 Spring MVC 消息转换器读取请求体之前。当 @RequestBody 触发反序列化时,拿到的输入流已经是明文 JSON——无论是 HeartbeatPackage 还是 BaseRequestPackage,Controller 完全感知不到加密的存在。
6.2 响应加密:ResponsePackageEncryptAdvice
@RestControllerAdvice(basePackages = "com.gitee.pifeng.monitoring.server.business.server.controller")
public class ResponsePackageEncryptAdvice implements ResponseBodyAdvice<Object> {
@Override
public Object beforeBodyWrite(Object body, ...) {
if (null != body) {
return new HttpOutputMessagePackageEncrypt().encrypt(body);
}
return null;
}
}
beforeBodyWrite 在 Spring MVC 将返回值写回客户端之前拦截,把明文对象序列化为 JSON 后加密,封装为 CiphertextPackage 返回。
6.3 全局异常处理
ResponsePackageEncryptAdvice 还承担了全局异常处理的职责:
@ExceptionHandler(value = Throwable.class)
public CiphertextPackage handler(Throwable throwable, HttpServletRequest request) {
String clientAddress = AccessObjectUtils.getClientAddress(request);
String uri = request.getRequestURI();
log.error("请求客户端IP:{},URI:{},异常:{}", clientAddress, uri, throwableString);
Result build = Result.builder().isSuccess(false).msg(throwableString).build();
BaseResponsePackage baseResponsePackage = this.serverPackageConstructor.structureBaseResponsePackage(build);
return new HttpOutputMessagePackageEncrypt().encrypt(baseResponsePackage);
}
这里直接返回了 CiphertextPackage 类型——手动完成加密,绕过了 beforeBodyWrite 的逻辑。这确保了即使 Controller 抛出异常,响应也始终是加密的密文,不会泄露内部错误细节。
6.4 完整的加密管道
将请求解密和响应加密串联起来:
请求方 发送 CiphertextPackage(密文)
│
▼
RequestPackageDecryptAdvice.beforeBodyRead()
│ → 解密 + 可选解压 → 明文 JSON
▼
Spring MVC HttpMessageConverter
│ 反序列化为具体的 Package 对象
▼
Controller 方法 ← 拿到的是明文对象
│ 处理业务 → 返回 BaseResponsePackage(明文)
▼
ResponsePackageEncryptAdvice.beforeBodyWrite()
│ → JSON 序列化 → 可选压缩 + 加密 → CiphertextPackage
▼
返回 CiphertextPackage(密文)给请求方
三类 Controller 都在这套管道中运作——数据接收、主动探测、命令下发,无一例外。
七、AOP 监听器回调:事件驱动的设计
数据接收类 Controller 还有一套基于 AOP 的监听器回调机制。以 ServerAspect 为例:
@Deprecated
@Aspect
@Component
public class ServerAspect {
@Autowired(required = false)
private List<ILinkListener> linkListeners;
@Autowired(required = false)
private List<IServerListener> serverListeners;
@Pointcut("execution(public * ...ServerController.acceptServerPackage(..))")
public void tangentPoint() {}
@Before("tangentPoint()")
public void beforeWakeUp(JoinPoint joinPoint) {
ServerPackage serverPackage = (ServerPackage) joinPoint.getArgs()[0];
if (this.linkListeners != null) {
this.linkListeners.forEach(o ->
ThreadPool.getCommonIoIntensiveThreadPoolExecutor().execute(() -> {
o.wakeUpMonitor(serverPackage);
}));
}
}
@After("tangentPoint()")
public void afterWakeUp(JoinPoint joinPoint) {
ServerPackage serverPackage = (ServerPackage) joinPoint.getArgs()[0];
String ip = serverPackage.getIp();
if (this.serverListeners != null) {
this.serverListeners.forEach(o ->
ThreadPool.getCommonIoIntensiveThreadPoolExecutor().execute(() -> {
o.wakeUpMonitor(ip);
}));
}
}
}
每个 Aspect 的切入点精确绑定到对应的 Controller 方法:
| 切面 | 切入点 | 监听器 | 通知类型 |
|---|---|---|---|
| HeartbeatAspect | HeartbeatController.acceptHeartbeatPackage | ILinkListener | @Before |
| ServerAspect | ServerController.acceptServerPackage | ILinkListener + IServerListener | @Before + @After |
| OfflineAspect | OfflineController.acceptOfflinePackage | IOfflineListener | @Before |
| NetworkDeviceAspect | NetworkDeviceController.acceptNetworkDevicePackage | INetworkDeviceListener | @After |
| ExceptionLogAspect | server 包下所有方法 | IAlarmService + ILogExceptionService | @AfterThrowing |
有两个设计细节值得注意:
监听器通过 @Autowired(required = false) 注入。 如果没有注册任何监听器实现类,Spring 不会报错,Aspect 内部通过 if (listeners != null) 判空跳过。这是典型的「可选扩展点」设计——系统核心流程不依赖监听器,但留出了扩展接口。
监听器回调通过线程池异步执行。 所有 wakeUpMonitor 调用都提交到 ThreadPool.getCommonIoIntensiveThreadPoolExecutor(),不会阻塞 Controller 方法的主流程。在高并发场景下,监听器的处理耗时不会影响数据接收的响应速度。
ExceptionLogAspect 比较特殊——它的切入点覆盖了 server 包下的所有方法,不仅限于 Controller。任何 Service 或 Component 抛出异常时,它都会记录异常日志、构建告警、通过 IAlarmService 发送告警通知。它还通过 ThreadLocal 标记防止重入,避免切面内部的告警发送逻辑再次抛异常时触发无限递归。
八、ServerPackageConstructor:响应的统一构造
所有 Controller 都注入了 ServerPackageConstructor,它负责将业务层的 Result 封装为完整的 BaseResponsePackage:
@Component
public class ServerPackageConstructor extends AbstractPackageConstructor {
@SneakyThrows
@Override
public BaseResponsePackage structureBaseResponsePackage(Result result) {
BaseResponsePackage baseResponsePackage = new BaseResponsePackage();
this.structureBaseResponsePackage(baseResponsePackage, result);
return baseResponsePackage;
}
private <T extends BaseResponsePackage> void structureBaseResponsePackage(T pkg, Result result) throws NetException {
// 自动填充:端点类型、实例ID、实例名、IP、计算机名、链路信息
this.structureAbstractSuperPackage(pkg);
pkg.setId(IdUtil.randomUUID());
pkg.setDateTime(new Date());
pkg.setResult(result);
}
}
structureAbstractSuperPackage 会自动填充服务端的身份信息——端点类型(SERVER)、实例 ID、实例名称、IP 地址、链路信息等。调用方只需传入业务 Result,无需关心响应包的元数据构造。
这个设计的关键价值在于:响应包的元数据是自动注入的、不可遗漏的。 如果让每个 Controller 手动构造响应包,很容易忘记设置某个字段(比如链路信息),导致追踪断链。
对于命令下发和线程池配置,ServerPackageConstructor 还提供了 structureJavaThreadPoolPackage 等专用构造方法,将业务对象包装为完整的请求包用于 WebSocket 发送。
九、小结
本篇全面分析了服务端 Controller 层的设计,核心要点如下:
- 三大类 Controller 各司其职:数据接收(专用强类型 Package)、主动探测(通用 BaseRequestPackage + extraMsg)、配置与命令(配置热刷新、WebSocket 命令转发、线程池动态配置)
- 数据接收范式统一:「计时 → 委托 Service → 构造响应 → 慢处理告警」四步范式,Controller 只做调度不碰业务
- 主动探测灵活务实:连通性测试覆盖 DB/HTTP/TCP/网络/网络设备五大类,数据库运维操作按类型拆分 Controller(Oracle/MySQL/Redis/Mongo)
- 命令下发的中转架构:UI → CommandController → Service(查库获取代理端地址)→ WebSocket 同步发送 → 代理端执行,
MonitorTypeEnums为扩展预留了空间 - 配置热刷新:数据库驱动 + 手动触发 + 自动定时三重机制,
synchronized保证并发安全 - 加密管道:通过
RequestBodyAdvice/ResponseBodyAdvice在框架层透明完成加解密,三类 Controller 共享同一管道,零代码侵入 - AOP 监听器回调:数据接收类 Controller 配套 Aspect,通过线程池异步触发监听器;
ExceptionLogAspect覆盖全局异常捕获与告警 - 渐进式演进:HTTP 数据接收 Controller 标注
@Deprecated向 WebSocket 迁移,主动探测和命令类保持 HTTP 模式不变
下一篇我们将深入 Service 层的并行数据处理机制,看看服务端收到一个包含十几个子指标的服务器信息包后,是如何通过 CompletableFuture 异步编排实现高效持久化的。
项目地址:
https://gitcode.com/monitoring-platform/phoenix
https://gitee.com/monitoring-platform/phoenix
https://github.com/709343767/phoenix
