目录

    Phoenix监控平台技术解析(二十三):服务端数据接收架构——Controller层设计

    上一篇我们拆解了代理端的数据转发机制——采集到的数据经过 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;
        }
    }
    

    同样的模式在 HttpControllerTcpControllerNetworkControllerNetworkDeviceController.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 提供了两个查询接口(按文件/按表空间),DbInfo4RedisServiceControllerDbInfo4MongoServiceController 分别查询 Redis 和 MongoDB 的运行信息。这些 Controller 按数据库类型拆分,而不是合并为一个「通用数据库 Controller」——因为每种数据库的连接方式、查询语句、返回结构完全不同,强行统一反而增加复杂度。

    3.4 数据接收与主动探测的响应差异

    两类 Controller 在响应构造上有一个微妙但重要的差异:

    数据接收类的 Service 返回 Result,Controller 用 serverPackageConstructor.structureBaseResponsePackage(result) 包装。Service 负责业务判断,Result.isSuccess 可能为 true 也可能为 false

    主动探测类的 Controller 直接构造 ResultisSuccess 固定为 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 转发结果构造成功/失败响应;而 InstanceControllerMonitoringPropertiesConfigController 仍然由 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

    欢迎关注微信公众号获取更多技术干货
    微信公众号·披锋斩棘

    end
    站长头像 知录

    你一句春不晚,我就到了真江南!

    文章0
    浏览0

    文章分类

    标签云