diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml
index 4644326e6e..62105f110a 100644
--- a/.github/workflows/maven-build.yml
+++ b/.github/workflows/maven-build.yml
@@ -24,10 +24,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Set up JDK 8
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: '8'
+ java-version: '21'
distribution: 'temurin'
cache: 'maven'
diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml
index 4fe4df9144..ca62d454f4 100644
--- a/.github/workflows/maven-deploy.yml
+++ b/.github/workflows/maven-deploy.yml
@@ -17,10 +17,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Set up JDK 8
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: '8'
+ java-version: '21'
distribution: 'temurin'
cache: 'maven'
server-id: ossrh
@@ -43,4 +43,4 @@ jobs:
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_TOKEN }}
- MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
\ No newline at end of file
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
diff --git a/JDK21_UPGRADE_PLAN.md b/JDK21_UPGRADE_PLAN.md
new file mode 100644
index 0000000000..2f68e3597f
--- /dev/null
+++ b/JDK21_UPGRADE_PLAN.md
@@ -0,0 +1,210 @@
+# CAT JDK 21 升级计划
+
+## 目标
+
+将当前 CAT 工程升级到 JDK 21 构建和运行,替换高风险历史依赖,并逐步从“外置 Tomcat + WAR”部署方式迁移到“JDK 21 + 可执行 JAR + 内嵌容器”部署方式。
+
+这次升级不只改版本号。项目仍依赖 `javax.servlet`、`web.xml`、Unidal MVC、Plexus 组件、JSP、历史 Log4j 1.x API 和 MySQL/Netty/Hadoop 等基础库。升级需要分阶段推进,每个阶段都要保持可编译、可测试、可打包、可启动。
+
+## 当前推荐路线
+
+推荐采用“两步容器迁移”:
+
+1. 先把工程构建、运行时和主要基础依赖升级到 JDK 21 可用状态。
+2. 新增 `cat-boot` 过渡模块,用 Spring Boot 4 稳定版作为启动外壳,但 Web 容器暂时锁定在 Tomcat 9 线,继续兼容现有 `javax.servlet` / JSP / Unidal MVC。
+3. Docker 从外置 Tomcat 镜像切换为 JDK 21 runtime 镜像,通过 `java -jar /app/cat-boot.jar` 启动。
+4. 后续单独评估 Jakarta 迁移,确认 Unidal MVC、JSP、Servlet Filter 和插件体系可行后,再考虑真正切换到 Spring Boot 4 原生 Jakarta Web 栈。
+
+不建议第一步直接把现有 Web 层 Jakarta 化。Spring Boot 4 的 Web 生态基于 Jakarta Servlet,而当前项目大量代码和依赖仍是 `javax.servlet`,直接切换会把 JDK、容器、MVC、JSP、Filter、依赖树问题混在一起,风险过高。
+
+## 已完成的升级项
+
+### 1. JDK 21 构建基线
+
+- 根 POM 已改为 `21`。
+- Maven Enforcer 已要求 Java `[21,22)` 和 Maven `[3.9.0,)`。
+- Maven 插件已升级到 JDK 21 兼容版本,包括 compiler、surefire、war、shade、source、javadoc、enforcer。
+- GitHub Actions 已从 JDK 8 切换到 JDK 21。
+- Docker 构建镜像已切换到 `maven:3.9.12-eclipse-temurin-21`。
+
+### 2. 测试兼容性修复
+
+- 修复 `DefaultMessageTree.copyForTest()` 对消息长度头的处理,并释放 `ByteBuf`。
+- `PlainTextMessageCodec` 解码后补充完成态设置。
+- 多个 consumer analyzer 测试补充 `setCompleted()`。
+- `TaskHelperTest` 移除 PowerMock,用普通 JUnit/Mockito 风格验证。
+- `StateAnalyzer` 增加测试所需的 `setMIp(String ip)`。
+
+### 3. 日志组件迁移到 Logback
+
+- Logback 已升级到 `1.5.34`。
+- SLF4J 使用 `2.0.17` 稳定线。
+- 服务端模块引入 `logback-classic` 和 `log4j-over-slf4j`。
+- 排除 Hadoop 传递进来的旧 `slf4j-log4j12` 和 `log4j`。
+- 新增 `cat-home/src/main/resources/logback.xml`。
+- 测试模块新增 `logback-test.xml` 或测试用 `log4j.properties`,避免测试日志绑定冲突。
+- 保留 `log4j:log4j` 的 optional/provided 兼容依赖,用于 CAT 客户端对外暴露的 Log4j 1.x Appender API;它不应作为服务端运行时日志实现。
+
+### 4. JSON 组件迁移到 fastjson2
+
+- `com.alibaba:fastjson` 已替换为 `com.alibaba.fastjson2:fastjson2`。
+- fastjson2 已升级到 `2.0.62`。
+- 当前源码未发现直接使用 `com.alibaba.fastjson.*` import,本阶段主要是依赖坐标迁移。
+
+### 5. 低风险依赖升级
+
+- JUnit 升级到 `4.13.2`。
+- Netty 升级到 `4.1.128.Final`,不采用 Netty 5 alpha。
+- Gson 升级到 `2.13.2`。
+- HttpClient/HttpMime 升级到 `4.5.14`。
+- Commons Codec 升级到 `1.19.0`。
+- Snappy 升级到 `1.1.10.8`。
+- Freemarker 升级到 `2.3.34`。
+- c3p0 升级到 `0.11.2`。
+- Plexus Utils 升级到 `4.0.2`。
+- java-saml 升级到 `2.9.0`。
+- MySQL 驱动坐标从 `mysql:mysql-connector-java` 切换为 `com.mysql:mysql-connector-j:9.7.0`。
+- MySQL 驱动类配置更新为 `com.mysql.cj.jdbc.Driver`。
+
+### 6. Spring Boot 过渡启动模块
+
+- 新增 `cat-boot` 模块。
+- `cat-boot` 引入 Spring Boot `4.0.2` 稳定版作为启动外壳。
+- `cat-boot` 显式使用 `tomcat-embed-core` / `tomcat-embed-jasper` `9.0.112`,继续兼容现有 `javax.servlet` Web 层。
+- `cat-boot` 打包时复制 `cat-home.war` 到 classpath,并通过 shade 生成可执行 JAR。
+- `CatBootApplication` 启动 Spring Boot 后,由 `EmbeddedCatServer` 解出 `cat-home.war` 并部署到 `/cat`。
+- shade 配置已排除 `META-INF/*.SF`、`META-INF/*.DSA`、`META-INF/*.RSA`,避免 JDK 21 下 fat jar 签名校验失败。
+
+### 7. Docker 运行方式切换
+
+- Docker runtime 镜像已从 Tomcat/JRE 8 切换为 `eclipse-temurin:21-jre`。
+- Docker 运行入口已改为 `java -Dcat.home=/data/appdatas/cat -Dserver.port=8080 -jar /app/cat-boot.jar`。
+- `docker-compose.yml` 已改为基于当前 Dockerfile 构建 `cat:4.0-RC1-jdk21`,不再默认拉取旧的 `meituaninc/cat:3.0.1` 镜像。
+
+## 当前验证结果
+
+已通过的验证:
+
+- `mvn -pl cat-consumer -am test`
+- `mvn -pl cat-alarm -am test`
+- `mvn -pl cat-boot -am package -DskipTests`
+- `mvn package -DskipTests`
+- `mvn test`
+- `java -Dserver.port=0 -jar cat-boot/target/cat-boot-4.0-RC1.jar` 短启动验证
+- `java -Dcat.home=.tmp-cat-home -Dserver.port=18080 -jar cat-boot/target/cat-boot-4.0-RC1.jar` 短启动和 HTTP 探测
+
+短启动中已确认:
+
+- Spring Boot `4.0.2` 启动。
+- Embedded Tomcat `9.0.112` 启动。
+- CAT Web 应用部署到 `/cat`。
+- Netty receiver 启动日志出现。
+- 本地未配置 `datasources.xml` / `server.xml` 时会出现数据源缺失日志,这是运行环境配置问题,不是构建失败。
+- 使用临时 `cat.home` 访问 `/cat/r` 可到达 Web 层,但返回 500;堆栈指向缺少数据源和 top service 注册,不是 Spring Boot/Tomcat 启动失败。
+- 当前机器未安装或未暴露 `docker` 命令,Docker build/compose 端到端验证尚未执行。
+
+## 待完成验证
+
+### 1. 全量打包复验
+
+在最终提交前继续执行:
+
+```bash
+mvn package -DskipTests
+```
+
+通过后确认:
+
+- `cat-home/target/cat-home-4.0-RC1.war` 存在。
+- `cat-boot/target/cat-boot-4.0-RC1.jar` 存在。
+
+### 2. Docker 构建验证
+
+需要在可用 Docker 环境中执行:
+
+```bash
+docker build -f docker/Dockerfile .
+```
+
+验收标准:
+
+- Maven 构建阶段成功。
+- runtime 镜像只依赖 JDK 21 runtime,不依赖外置 Tomcat。
+- 镜像内存在 `/app/cat-boot.jar`。
+
+### 3. Docker Compose 端到端验证
+
+需要执行:
+
+```bash
+docker compose -f docker/docker-compose.yml up --build
+```
+
+验收标准:
+
+- MySQL 容器启动并初始化 `cat` schema。
+- CAT 容器启动成功。
+- `8080` Web 端口可访问。
+- `2280` TCP 接收端口可监听。
+- `/cat/r`、`/cat/s` 等路由可访问。
+- JSP 页面和静态资源可正常加载。
+- Logback 日志正常输出。
+- JSON 输出结构保持兼容。
+
+### 4. 带真实配置的运行冒烟
+
+需要提供或挂载真实配置:
+
+- `client.xml`
+- `server.xml`
+- `datasources.xml`
+
+验收标准:
+
+- MySQL 数据源初始化成功。
+- 服务端能写入和读取 CAT 配置。
+- 客户端上报链路可打通。
+- 报表页面可访问。
+- 告警配置和通知链路无启动异常。
+
+## 仍需单独评估的升级项
+
+### 1. Jakarta / Spring Boot 原生 Web 栈迁移
+
+当前 `cat-boot` 是过渡方案,不等于已经完成 Spring Boot 4 原生 Web 迁移。后续需要单独评估:
+
+- `javax.servlet.*` 到 `jakarta.servlet.*` 的代码迁移。
+- `web.xml` namespace 和 Servlet/JSP/JSTL 依赖迁移。
+- Unidal MVC 是否支持 Jakarta。
+- Plexus 组件初始化顺序是否受影响。
+- JSP 在新容器中的渲染兼容性。
+- Filter dispatch、权限过滤、Domain 过滤是否保持兼容。
+
+### 2. Unidal 升级
+
+Maven 版本检查显示 Unidal framework 有 4.x 线,但它影响 MVC、Plexus、DAL、代码生成和运行时组件,不应和当前 JDK/Spring Boot 过渡升级混在一起。建议后续单独开分支验证。
+
+### 3. Hadoop 升级
+
+Hadoop `2.4.1` 到 3.x 是高风险升级,会影响 HDFS/logview 相关行为和传递依赖。建议单独处理,并补充 HDFS 场景验证。
+
+### 4. 历史兼容 API
+
+CAT 客户端仍对外提供 Log4j 1.x Appender 等历史集成 API。服务端运行时可以迁移到 Logback,但客户端 API 是否彻底移除需要单独评估兼容性和用户影响。
+
+## 提交拆分建议
+
+建议按以下顺序拆分提交:
+
+1. JDK 21 构建基线、Maven 插件、CI 配置。
+2. JDK 21 下的测试修复。
+3. Logback 迁移。
+4. fastjson2 迁移。
+5. 低风险依赖升级。
+6. 新增 `cat-boot` 过渡启动模块。
+7. Docker 从外置 Tomcat 切换为 `java -jar`。
+8. 升级计划文档和验证记录。
+
+## 当前结论
+
+当前推荐方案已经进入可继续验证状态:JDK 21 构建、Logback、fastjson2、低风险依赖和 `cat-boot` 可执行 JAR 已完成初步升级。接下来重点不是继续扩大版本升级范围,而是完成 Docker/MySQL/JSP/路由/上报链路的端到端验证,并把 Jakarta 迁移作为后续独立阶段处理。
diff --git a/SPRING_CONTEXT_MIGRATION_HANDOFF_2026-06-07.md b/SPRING_CONTEXT_MIGRATION_HANDOFF_2026-06-07.md
new file mode 100644
index 0000000000..1eb3feb028
--- /dev/null
+++ b/SPRING_CONTEXT_MIGRATION_HANDOFF_2026-06-07.md
@@ -0,0 +1,910 @@
+# CAT Spring 上下文迁移交接文档(2026-06-07)
+
+## 1. 文档目的
+
+这份文档用于下一次任务开始时快速恢复上下文。下一次继续迁移前,优先阅读本文,再结合当前 `git status --short` 和最近编译结果判断从哪一步继续。
+
+当前项目路径:
+
+```text
+D:\workspace\cat
+```
+
+当前总体目标:
+
+```text
+逐步废弃 Plexus / Unidal Lookup / Unidal DAL 运行时依赖,让应用上下文、数据访问、Service、Manager、后台任务和 Web 层逐步迁移到 Spring 体系。
+```
+
+当前迁移策略:
+
+1. 不一次性删除 Plexus。
+2. 先把 DAO / Repository / 配置 Manager / 基础 Service 接入 Spring。
+3. 旧 Unidal MVC Handler 暂时保留,由 Handler 通过桥接方式使用 Spring Bean。
+4. 每一步完成后都编译、打包、启动验证。
+5. 不确定运行结果时,停下来让用户在 IDEA 或本地环境验证。
+
+## 2. 当前可运行状态
+
+### 2.1 最新进度快照(本节优先)
+
+截至 2026-06-07 最近一轮迁移,配置页 Processor 的 Spring 依赖刷新和一批配置 Manager 的 Spring 注册已经完成,并通过编译、打包、临时启动和多条配置页 URL 验证。
+
+本节是最新状态摘要;如果后文旧计划与本节冲突,以本节为准。
+
+已完成的最新迁移点:
+
+1. `CatHomeSpringConfiguration` 继续扩展,新增注册了 `ConfigHtmlParser`、`RouterConfigManager`、`DomainGroupConfigManager`、`StorageGroupConfigManager`、`TopologyGraphConfigManager`、`TopoGraphFormatConfigManager`、`HeartbeatDisplayPolicyManager`、`ExceptionRuleConfigManager`、`AlertConfigManager`、`AlertPolicyManager`、`BaseRuleHelper`、`TransactionRuleConfigManager`、`EventRuleConfigManager`、`HeartbeatRuleConfigManager`。
+2. 多个配置 Manager 已补 setter,使 Spring 初始化不再依赖 `CatSpringContext` 在 `context.refresh()` 过程中已经可用。
+3. 配置页 Processor 已增加执行前刷新 Spring 依赖的逻辑,但 Processor 本体仍由 Plexus 创建。
+4. `TimerSyncTask.register` 已按 handler name 去重,避免 Plexus 和 Spring 双实例初始化时重复注册同名同步 handler。
+5. 最新一次启动日志显示 Spring Home 侧 `beanCount=90`。
+
+当前工作树中,本轮迁移相关 Java 文件仍处于未提交修改状态。不要回滚这些改动,下一次继续前先用 `git status --short` 确认:
+
+```text
+cat-alarm/src/main/java/com/dianping/cat/alarm/spi/config/AlertConfigManager.java
+cat-alarm/src/main/java/com/dianping/cat/alarm/spi/config/AlertPolicyManager.java
+cat-core/src/main/java/com/dianping/cat/task/TimerSyncTask.java
+cat-home/src/main/java/com/dianping/cat/home/spring/CatHomeSpringConfiguration.java
+cat-home/src/main/java/com/dianping/cat/report/alert/exception/ExceptionRuleConfigManager.java
+cat-home/src/main/java/com/dianping/cat/report/alert/spi/config/BaseRuleConfigManager.java
+cat-home/src/main/java/com/dianping/cat/report/page/DomainGroupConfigManager.java
+cat-home/src/main/java/com/dianping/cat/report/page/dependency/config/TopoGraphFormatConfigManager.java
+cat-home/src/main/java/com/dianping/cat/report/page/dependency/graph/TopologyGraphConfigManager.java
+cat-home/src/main/java/com/dianping/cat/report/page/heartbeat/config/HeartbeatDisplayPolicyManager.java
+cat-home/src/main/java/com/dianping/cat/report/page/storage/config/StorageGroupConfigManager.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/AlertConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/BaseProcesser.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/DependencyConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/EventConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/ExceptionConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/GlobalConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/HeartbeatConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/StorageConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/TransactionConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/router/config/RouterConfigManager.java
+```
+
+截至本交接文档编写时,用户已经多次确认项目可以正常启动和访问,最近一次用户反馈是:
+
+```text
+已验证 ok / 没问题,继续。
+```
+
+已知当前运行形态:
+
+1. JDK 已升级到 21。
+2. 项目通过 `cat-boot` 以可执行 JAR 启动。
+3. `cat-boot` 内嵌 Tomcat,部署 `cat-home.war` 到 `/cat`。
+4. 外置 Tomcat 已废弃。
+5. Logback 已替换旧日志实现。
+6. JSON 依赖已迁移到 fastjson2。
+7. Lombok 已引入。
+8. MyBatis 已覆盖 `CatApplication.sql` 中的表,并已有大量 Repository 迁移。
+9. Spring 上下文已建立,并能被旧 Plexus 对象通过 `CatSpringContext` 桥接访问。
+
+常用启动参数:
+
+```text
+-Dcat.home=C:\Users\Shang\.cat
+-Dcat.log.path=C:\Users\Shang\.cat\logs
+```
+
+常用访问入口:
+
+```text
+http://127.0.0.1:8080/cat
+http://127.0.0.1:8080/cat/r
+http://127.0.0.1:8080/cat/s/config
+http://127.0.0.1:8080/cat/s/router?op=json&domain=cat&ip=127.0.0.1
+```
+
+临时验证启动建议使用非默认端口,避免和用户 IDEA 启动实例冲突:
+
+```powershell
+java -Dserver.port=18080 -Dcat.tcp.port=12280 -jar cat-boot\target\cat-boot-4.0-RC1.jar
+```
+
+自己启动的 Java 进程验证完成后必须停止。
+
+## 3. 今天左右已经完成的主要改动
+
+### 3.1 JDK 21 和 Spring Boot 启动外壳
+
+已完成:
+
+1. 根 POM 和模块 POM 已适配 JDK 21。
+2. 新增 `cat-boot` 模块。
+3. `CatBootApplication` 已作为 Spring Boot 启动入口。
+4. `cat-boot` 打包时包含并部署 `cat-home.war`。
+5. 默认系统参数已补齐:
+
+```text
+cat.home=C:\Users\Shang\.cat
+cat.log.path=C:\Users\Shang\.cat\logs
+```
+
+相关重点文件:
+
+```text
+cat-boot/src/main/java/com/dianping/cat/boot/CatBootApplication.java
+cat-boot/src/main/java/com/dianping/cat/boot/config/
+```
+
+### 3.2 日志迁移
+
+已完成:
+
+1. 日志实现切换为 Logback。
+2. 引入新版 Logback。
+3. 增加 `logback.xml`。
+4. 参考 `D:\workspace\lawyer-system` 的风格补充了日志文件输出配置。
+5. 处理过错误日志没有落入 `error.log` 的问题。
+
+日志目录期望位置:
+
+```text
+C:\Users\Shang\.cat\logs
+```
+
+注意:
+
+1. 控制台仍可能出现 CAT 客户端连接自身服务端的 WARN。
+2. Logback scan 相关 “Missing watchable .xml” 警告可以暂时忽略。
+3. 是否进入 `error.log` 取决于 logger、appender 和 level 配置,后续继续改日志时要实际触发 ERROR 验证。
+
+### 3.3 fastjson2 和 Lombok
+
+已完成:
+
+1. JSON 依赖迁移为 fastjson2。
+2. 项目中直接 `com.alibaba.fastjson.*` import 已检查并处理。
+3. Lombok 已引入依赖和编译配置。
+
+### 3.4 MyBatis DAO / Repository 迁移
+
+已完成:
+
+1. 基于 `CatApplication.sql` 为所有表生成 MyBatis DO / Mapper / XML。
+2. `config` 表迁移为独立 MyBatis DAO,不再继承旧 DAO。
+3. `dailyreport` 表迁移到 MyBatis。
+4. 后续已扩展到所有表。
+5. 新 DAO 包和命名已按用户要求调整,例如 `ConfigMapper`、`ConfigDO`。
+6. 目标是新 DAO 层不依赖旧框架。
+
+当前重要结构:
+
+```text
+cat-core/src/main/java/com/dianping/cat/core/mybatis/
+cat-core/src/main/resources/mybatis/mapper/
+cat-home/src/main/resources/mybatis/mapper/
+cat-alarm/src/main/resources/mybatis/mapper/
+```
+
+已知语义:
+
+1. `hostinfo` 表为空是正常场景,不应被当作错误。
+2. 查询不到数据时要尽量保持旧语义,例如该抛 `DalNotFoundException` 的地方不能被统一包装成普通 `DalException`。
+
+### 3.5 Spring 上下文基础设施
+
+已完成:
+
+1. 新增 Spring 上下文桥接类:
+
+```text
+cat-core/src/main/java/com/dianping/cat/spring/CatSpringContext.java
+```
+
+2. 新增 Spring Home 配置:
+
+```text
+cat-home/src/main/java/com/dianping/cat/home/spring/CatHomeSpringConfiguration.java
+```
+
+3. `CatSpringContext` 支持旧对象通过类型获取 Spring Bean:
+
+```java
+CatSpringContext.getBeanIfAvailable(SomeClass.class)
+```
+
+4. `CatHomeSpringConfiguration` 已注册:
+
+```text
+DataSource
+SqlSessionFactory
+SqlSessionTemplate
+TransactionManager
+TransactionTemplate
+ConfigRepository
+ProjectRepository
+HostinfoRepository
+各类 Report Repository
+各类告警/配置相关 Repository
+ProjectService
+BusinessConfigManager
+ServerConfigManager
+SampleConfigManager
+ServerFilterConfigManager
+ReportReloadConfigManager
+AtomicMessageConfigManager
+TpValueStatisticConfigManager
+AlertSummaryService
+UserDefinedRuleManager
+BaselineService
+RemoteServersManager
+DomainValidator
+ServerStatisticManager
+ConfigHtmlParser
+RouterConfigManager
+DomainGroupConfigManager
+StorageGroupConfigManager
+TopologyGraphConfigManager
+TopoGraphFormatConfigManager
+HeartbeatDisplayPolicyManager
+ExceptionRuleConfigManager
+AlertConfigManager
+AlertPolicyManager
+BaseRuleHelper
+TransactionRuleConfigManager
+EventRuleConfigManager
+HeartbeatRuleConfigManager
+```
+
+最近一次启动验证看到 Spring Home 侧 Bean 数量约为:
+
+```text
+beanCount=90
+```
+
+### 3.6 Repository 和 Service 桥接
+
+已完成并验证的桥接点包括:
+
+```text
+AbstractReportService
+TaskManager
+DefaultTaskConsumer
+system/page/config/Handler 的 ConfigModificationRepository
+AlertSummaryService
+UserDefinedRuleManager
+BaseRuleConfigManager
+RelatedSummaryBuilder
+BusinessBaselineReportBuilder
+AbstractGraphCreator
+```
+
+已注册或桥接的 Repository / Service 包括:
+
+```text
+AlertRepository
+AlterationRepository
+BaselineRepository
+TopologyGraphRepository
+TaskRepository
+AlertSummaryRepository
+ConfigModificationRepository
+MetricGraphRepository
+MetricScreenRepository
+ServerAlarmRuleRepository
+UserDefineRuleRepository
+AlertSummaryService
+UserDefinedRuleManager
+BaselineService(DefaultBaselineService)
+```
+
+这些改动的核心思路:
+
+```text
+旧对象仍由 Plexus 创建,但运行前优先从 Spring 上下文刷新已迁移 Bean。
+```
+
+这样可以避免一次性让 Spring 接管所有旧组件造成双容器初始化或字段注入为空。
+
+## 4. 最近已经验证过的命令
+
+常用编译验证:
+
+```powershell
+mvn -pl cat-home -am compile -DskipTests
+```
+
+常用打包验证:
+
+```powershell
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+常用临时启动验证:
+
+```powershell
+java -Dserver.port=18080 -Dcat.tcp.port=12280 -jar cat-boot\target\cat-boot-4.0-RC1.jar
+```
+
+常用 HTTP 验证:
+
+```text
+http://127.0.0.1:18080/cat
+http://127.0.0.1:18080/cat/r
+http://127.0.0.1:18080/cat/s/config
+http://127.0.0.1:18080/cat/s/router?op=json&domain=cat&ip=127.0.0.1
+```
+
+最近一轮已经额外验证过的配置页和路由入口:
+
+```text
+http://127.0.0.1:18080/cat/s/config?op=routerConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=domainGroupConfigs
+http://127.0.0.1:18080/cat/s/config?op=storageGroupConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=serverConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=sampleConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=topologyGraphNodeConfigList
+http://127.0.0.1:18080/cat/s/config?op=topologyGraphEdgeConfigList
+http://127.0.0.1:18080/cat/s/config?op=topoGraphFormatConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=heartbeatDisplayPolicy
+http://127.0.0.1:18080/cat/s/config?op=heartbeatRuleConfigList
+http://127.0.0.1:18080/cat/s/config?op=exception
+http://127.0.0.1:18080/cat/s/config?op=alertPolicy
+http://127.0.0.1:18080/cat/s/config?op=alertDefaultReceivers
+http://127.0.0.1:18080/cat/s/config?op=transactionRule
+http://127.0.0.1:18080/cat/s/config?op=eventRule
+http://127.0.0.1:18080/cat/s/router?op=json&domain=cat&ip=127.0.0.1
+```
+
+可暂时忽略的日志:
+
+```text
+Unable to connect to CAT server /127.0.0.1:2280
+请求 http://127.0.0.1:8080/cat/s/router?... 失败
+Logback Missing watchable .xml...
+```
+
+端口注意:
+
+1. 默认 TCP 接收端口是 `2280`。
+2. 如果出现 `Address already in use: bind`,优先确认是否已有 CAT 实例或其他进程占用。
+3. 临时验证用 `-Dcat.tcp.port=12280` 可以绕开默认端口冲突。
+
+## 5. 当前正在推进但尚未完成的工作
+
+最新状态:
+
+```text
+配置页 Processor Spring 依赖刷新已经完成并验证。
+```
+
+本节下面保留的是迁移背景和当时的推进思路。下一次继续时不要再把“补 ConfigHtmlParser Spring Bean”或“给 Processor 增加 refreshSpringBeans()”当作未完成任务;这些已经完成。真正的下一步见文末“最新下一步计划”。
+
+当前最近一轮准备推进的是:
+
+```text
+配置页 Processor Spring 化。
+```
+
+目标不是马上删除 Plexus 注册,而是让配置页 Processor 在执行前刷新 Spring 已迁移依赖。
+
+涉及文件:
+
+```text
+cat-home/src/main/java/com/dianping/cat/system/page/config/Handler.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/BaseProcesser.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/GlobalConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/AlertConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/DependencyConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/HeartbeatConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/ExceptionConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/TransactionConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/EventConfigProcessor.java
+cat-home/src/main/java/com/dianping/cat/system/page/config/processor/StorageConfigProcessor.java
+```
+
+已经确认的现状:
+
+1. `Handler` 已有 `refreshSpringBeans()`,当前主要刷新 `ConfigModificationRepository`。
+2. `GlobalConfigProcessor` 已有 `refreshSpringBeans()`,当前刷新了:
+
+```text
+ProjectService
+SampleConfigManager
+ServerConfigManager
+ServerFilterConfigManager
+ReportReloadConfigManager
+```
+
+3. `GlobalConfigProcessor` 还可以继续刷新但尚未补齐的依赖:
+
+```text
+RouterConfigManager
+DomainGroupConfigManager
+StorageGroupConfigManager
+```
+
+这些 Manager 自身已具备 `refreshSpringBeans()` 能力,后续可考虑注册为 Spring Bean 或继续通过旧对象内部刷新。
+
+4. `AlertConfigProcessor` 依赖:
+
+```text
+AlertConfigManager
+AlertPolicyManager
+ConfigHtmlParser
+```
+
+这几个暂时不建议直接让 Spring 新建 Processor 后替换旧 Processor,除非先把所有字段依赖补齐。
+
+5. `DependencyConfigProcessor` 依赖:
+
+```text
+GlobalConfigProcessor
+TopologyGraphConfigManager
+TopoGraphFormatConfigManager
+ConfigHtmlParser
+```
+
+`TopologyGraphConfigManager` 和 `TopoGraphFormatConfigManager` 已经有内部 Spring 刷新 `ConfigRepository` / `ContentFetcher` 的逻辑。
+
+6. `HeartbeatConfigProcessor` 依赖:
+
+```text
+HeartbeatRuleConfigManager
+HeartbeatDisplayPolicyManager
+ConfigHtmlParser
+```
+
+`HeartbeatDisplayPolicyManager` 已经有内部 Spring 刷新逻辑;`HeartbeatRuleConfigManager` 继承 `BaseRuleConfigManager`,已能刷新 `ConfigRepository`、`ContentFetcher`、`UserDefinedRuleManager`。
+
+7. `ExceptionConfigProcessor` 依赖:
+
+```text
+GlobalConfigProcessor
+ExceptionRuleConfigManager
+```
+
+`ExceptionRuleConfigManager` 已有内部 Spring 刷新逻辑。
+
+8. `TransactionConfigProcessor` 和 `EventConfigProcessor` 依赖:
+
+```text
+TransactionRuleConfigManager
+EventRuleConfigManager
+RuleFTLDecorator(来自 BaseProcesser)
+```
+
+这两个 RuleConfigManager 继承 `BaseRuleConfigManager`,已能刷新部分 Spring 依赖。`RuleFTLDecorator` 暂时不建议急着 Spring 化,因为涉及模板装饰器和旧 components 配置。
+
+9. `StorageConfigProcessor` 当前基本是空操作,风险较低。
+
+## 6. 配置 Processor 迁移的推荐下一步
+
+注意:本节是配置 Processor 迁移前写下的推荐顺序,其中 6.2 和 6.3 已经执行完成。下一次继续请以文末“最新下一步计划”为准。
+
+下一次建议按下面顺序执行。
+
+### 6.1 不直接切换为 Spring 新建 Processor
+
+不要马上在 `CatHomeSpringConfiguration` 中注册 8 个 Processor 并让 Handler 使用 Spring Bean。
+
+原因:
+
+1. 当前 Processor 仍依赖大量 `@Inject` 字段。
+2. 如果 Spring `new Processor()`,这些 Plexus 字段不会自动注入。
+3. 直接替换会导致运行时 NPE,尤其是 `ConfigHtmlParser`、`RuleFTLDecorator`、各类 ConfigManager。
+
+推荐策略:
+
+```text
+Processor 仍由 Plexus 创建;Processor 执行前主动刷新 Spring 已迁移依赖。
+```
+
+### 6.2 先补 `ConfigHtmlParser` Spring Bean
+
+`ConfigHtmlParser` 是无状态工具类,可以低风险注册到 Spring。
+
+建议在:
+
+```text
+cat-home/src/main/java/com/dianping/cat/home/spring/CatHomeSpringConfiguration.java
+```
+
+增加:
+
+```java
+@Bean
+public ConfigHtmlParser configHtmlParser() {
+ return new ConfigHtmlParser();
+}
+```
+
+### 6.3 给 Processor 增加 `refreshSpringBeans()`
+
+建议补充:
+
+1. `BaseProcesser.refreshSpringBeans()`:
+
+```text
+可先不刷新 RuleFTLDecorator,避免模板相关风险。
+```
+
+2. `GlobalConfigProcessor.refreshSpringBeans()` 继续补:
+
+```text
+RouterConfigManager
+DomainGroupConfigManager
+StorageGroupConfigManager
+ConfigHtmlParser
+```
+
+3. `AlertConfigProcessor.refreshSpringBeans()`:
+
+```text
+ConfigHtmlParser
+如果 AlertConfigManager / AlertPolicyManager 已注册 Spring,再刷新;否则保留 Plexus 注入。
+```
+
+4. `DependencyConfigProcessor.refreshSpringBeans()`:
+
+```text
+GlobalConfigProcessor
+TopologyGraphConfigManager
+TopoGraphFormatConfigManager
+ConfigHtmlParser
+```
+
+这里要谨慎:如果 `GlobalConfigProcessor` 没有作为 Spring Bean 注册,就不要替换它;保留 Plexus 字段即可。
+
+5. `HeartbeatConfigProcessor.refreshSpringBeans()`:
+
+```text
+HeartbeatDisplayPolicyManager
+ConfigHtmlParser
+HeartbeatRuleConfigManager 如果已注册 Spring 再替换
+```
+
+6. `ExceptionConfigProcessor.refreshSpringBeans()`:
+
+```text
+ExceptionRuleConfigManager 如果已注册 Spring 再替换
+GlobalConfigProcessor 如果已注册 Spring 再替换
+```
+
+7. `TransactionConfigProcessor` / `EventConfigProcessor`:
+
+```text
+先只在 process() 开头调用 BaseProcesser.refreshSpringBeans()
+是否替换 RuleConfigManager 等确认 Spring Bean 注册后再做
+```
+
+### 6.4 Handler 只做刷新,不强切 Processor
+
+`Handler.refreshSpringBeans()` 下一步可以扩展成:
+
+```text
+刷新 ConfigModificationRepository
+必要时调用各 Processor 的 refreshSpringBeans()
+```
+
+但不建议直接:
+
+```java
+m_globalConfigProcessor = CatSpringContext.getBeanIfAvailable(GlobalConfigProcessor.class);
+```
+
+除非该 Processor 的所有依赖都已经由 Spring 明确注入或手动 setter 完成。
+
+## 7. 配置 Processor 迁移后的验证清单
+
+完成上述小步后,至少执行:
+
+```powershell
+mvn -pl cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+如果编译和打包通过,临时启动:
+
+```powershell
+java -Dserver.port=18080 -Dcat.tcp.port=12280 -jar cat-boot\target\cat-boot-4.0-RC1.jar
+```
+
+访问配置页:
+
+```text
+http://127.0.0.1:18080/cat/s/config
+http://127.0.0.1:18080/cat/s/config?op=serverConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=sampleConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=reportReloadConfigUpdate
+http://127.0.0.1:18080/cat/s/config?op=alertPolicy
+http://127.0.0.1:18080/cat/s/config?op=transactionRule
+http://127.0.0.1:18080/cat/s/config?op=eventRule
+http://127.0.0.1:18080/cat/s/config?op=heartbeatRuleConfigList
+```
+
+重点检查:
+
+1. 无 Processor 字段 NPE。
+2. 无 Mapper 未注册。
+3. 无 Spring Bean 循环依赖。
+4. 无重复初始化导致的任务重复启动。
+5. 配置页至少能返回页面或旧系统可接受的业务错误。
+
+验证结束后停止自己启动的 Java 进程。
+
+## 8. 后续总体计划
+
+### 8.1 短期计划:继续扩大 Spring 桥接面
+
+优先级从高到低:
+
+1. 配置 Processor 内部依赖刷新。
+2. 配置相关 Manager 注册或桥接到 Spring。
+3. 报表 Builder / Reloader / Task 继续桥接 Spring Repository 和 Service。
+4. 旧 Handler 逐步只保留请求分发和 JSP 渲染职责,业务依赖尽量来自 Spring。
+
+### 8.2 中期计划:迁移后台任务
+
+目标:
+
+```text
+用 Spring 生命周期和调度机制替代 Plexus 初始化阶段启动任务。
+```
+
+候选对象:
+
+```text
+TimerSyncTask
+ReportReloadTask
+DefaultTaskConsumer
+ProjectUpdateTask
+CurrentReportBuilder
+各类 ReportBuilder
+各类 ReportReloader
+CapacityUpdateStatusManager
+DailyCapacityUpdater
+HourlyCapacityUpdater
+WeeklyCapacityUpdater
+MonthlyCapacityUpdater
+```
+
+建议:
+
+1. 先加 Spring 调度基础设施。
+2. 每次只迁移一个任务族。
+3. 每个任务迁移后确认没有被 Plexus 和 Spring 重复启动。
+4. 任务迁移时一定要增加启动日志和停止逻辑。
+
+### 8.3 长期计划:迁移 Web 层
+
+最后再处理 Web 层,因为风险最大。
+
+当前旧链路:
+
+```text
+Unidal Web MVC -> Page Handler -> Payload -> Model -> JspViewer -> JSP
+```
+
+目标链路:
+
+```text
+Spring MVC Controller -> Service -> ModelAndView / JSP
+```
+
+建议顺序:
+
+1. 先迁移低风险页面。
+2. 保持原 URL 尽量不变。
+3. 每迁移一个页面就验证一个页面。
+4. 等 DAO / Service / Manager / Task 基本 Spring 化之后,再删除 Unidal MVC。
+
+## 9. 不要做的事
+
+下一次继续时注意:
+
+1. 不要直接删除 `components.xml` 或 Plexus 注册。
+2. 不要一次性把 `com.dianping.cat` 全量加入 Spring 扫描。
+3. 不要让 Spring 和 Plexus 同时创建同一个会启动线程或注册定时任务的组件。
+4. 不要直接用 Spring 新建 Processor 替换旧 Processor,除非确认字段依赖全部补齐。
+5. 不要回滚用户已有改动。
+6. 不要长期保留自己启动的 Java 进程。
+7. 不要把空表、查无数据这类旧系统允许的场景误判成迁移失败。
+
+## 10. 下一次任务开始建议执行的命令
+
+先看工作树:
+
+```powershell
+git status --short
+```
+
+查看旧容器依赖剩余面:
+
+```powershell
+rg -n "ContainerLoader|getDefaultContainer|lookup\(" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+rg -n "org\.unidal\.lookup\.annotation|@Named|Initializable|LogEnabled|ContainerHolder" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+rg -n "DataSourceManager|MyBatisRepositorySupport" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+```
+
+查看配置 Processor 当前状态:
+
+```powershell
+rg -n "refreshSpringBeans|class .*ConfigProcessor|class BaseProcesser" cat-home/src/main/java/com/dianping/cat/system/page/config/processor cat-home/src/main/java/com/dianping/cat/system/page/config/Handler.java
+```
+
+然后以第 11 节的最新下一步计划为准继续推进。
+
+## 11. 最新下一步计划(2026-06-07 更新)
+
+最终目标仍然是:
+
+```text
+移除 Plexus / Unidal Lookup,使用 Spring 管理所有 Bean。
+```
+
+当前已经完成到“配置页 Processor 执行前刷新 Spring 依赖 + 多个配置 Manager 注册为 Spring Bean”这一阶段。下一步不要急着删除 Plexus,也不要直接把 Handler 或 Processor 本体整体切到 Spring;应该继续扩大低风险 Bean 的 Spring 管理范围,并持续验证。
+
+推荐下一步顺序:
+
+1. 先检查当前未提交改动和编译状态:
+
+```powershell
+git status --short
+mvn -pl cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+2. 继续挑选低风险 Manager 注册到 Spring,优先看业务规则和业务告警相关对象,例如:
+
+```text
+BusinessRuleConfigManager
+业务告警相关 ConfigManager / PolicyManager
+只依赖 ConfigRepository、ContentFetcher、BaseRuleHelper、UserDefinedRuleManager 的 Manager
+```
+
+迁移方式沿用本轮做法:
+
+```text
+先补 setter -> 在 CatHomeSpringConfiguration 注册 Bean -> Plexus 旧对象执行前 refreshSpringBeans() -> 编译/打包/启动/HTTP 验证。
+```
+
+3. 暂缓把 `RuleFTLDecorator` 和配置 Processor 本体 Spring 化。`RuleFTLDecorator` 涉及模板装饰器和旧 components 注入,建议等更多 Manager 都稳定由 Spring 管理后再处理。
+
+4. 后台任务迁移前先继续观察 `TimerSyncTask`。本轮已经给 `TimerSyncTask.register` 增加同名 handler 去重,但任务类后续 Spring 化时仍必须确认没有双容器重复启动。
+
+5. 当配置 Manager 和后台任务基本稳定后,再迁移 Web 层:
+
+```text
+Unidal Handler 暂时保留请求分发。
+等依赖都可从 Spring 获取后,再逐步用 Spring MVC Controller 替换低风险页面。
+```
+
+最近一轮已通过的验证结果:
+
+```text
+mvn -pl cat-home -am compile -DskipTests BUILD SUCCESS
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true" BUILD SUCCESS
+临时启动 cat-boot,端口 18080 / TCP 12280 OK
+Spring Home beanCount 90
+多条 /cat/s/config 和 /cat/s/router URL HTTP 200
+```
+
+可接受的已知日志:
+
+```text
+Logback Missing watchable .xml
+Maven shade overlap warnings
+deprecated API warnings
+```
+
+不可忽略的日志:
+
+```text
+NullPointerException
+UnsatisfiedDependencyException
+NoSuchBeanDefinitionException
+Address already in use
+同一个任务或 SyncHandler 重复启动导致的重复执行
+```
+
+## 12. 最新进度快照(2026-06-13 更新)
+
+本节记录 2026-06-12 至 2026-06-13 的最新迁移进度。下次继续前,优先阅读本节,再结合 `git status --short` 判断工作区状态。
+
+### 12.1 当前 Git 状态
+
+截至本节更新时,工作区为干净状态:
+
+```powershell
+git status --short
+```
+
+输出为空。上一批代码已经由用户提交。
+
+### 12.2 已完成的最新迁移范围
+
+围绕“最终移除 Plexus / Unidal Lookup / Unidal 相关依赖”的目标,最近一批已经完成并提交的改动包括:
+
+1. `cat-home/src/main/java/com/dianping/cat/report/page` 下的 `@Named` / `@Inject` 已清空。
+2. 报表页面服务层已迁移到显式组件注册,覆盖:`transaction`、`event`、`problem`、`heartbeat`、`storage`、`cross`、`matrix`、`state`、`top`、`business`、`metric baseline`。
+3. 多个 `ReportService` 子类已经从注解注册迁移为 configurator 中的显式字段依赖注册。
+4. 多个 `LocalModelService` 子类已经从注解注册迁移为显式组件注册,并补齐 `m_bucketManager`、`m_configManager`、`m_consumer`。
+5. historical model service 的隐式依赖已经改为显式字段名依赖:`m_reportService`、`m_configManager`。
+6. 已迁移 `business` 相关组件:`BusinessReportService`、`LocalBusinessService`、`BusinessPointParser`、`BusinessKeyHelper`。
+7. 已迁移 `metric baseline` 相关组件:`DefaultBaselineService`、`BaselineConfigManager`、`DefaultBaselineCreator`。
+8. 已迁移 `report/graph/svg`:`GraphBuilder`、`ValueTranslater`、`DefaultGraphBuilder`、`DefaultValueTranslater`。
+9. 已迁移 `DataExtractorImpl`,改为 `DataExtractor` 接口显式注册。
+10. 已清理 `report/task/reload` 组的 Unidal 注解:`ReportReloadTask`、`AbstractReportReloader` 和所有 `*ReportReloader` 实现。
+
+`report/task/reload` 这一组已经由 `CatHomeSpringConfiguration` 通过 Spring Bean 和 setter 注入组装;生成的 `components.xml` 中没有对应 `ReportReloadTask` / `ReportReloader` 条目,因此只做注解清理,没有新增 Plexus 显式注册。
+
+### 12.3 最近验证结果
+
+最近一批迁移完成后,以下命令均已通过:
+
+```powershell
+mvn -pl cat-home -am -DskipTests compile
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+验证过程中 Maven 仍会出现已知 warning,例如 deprecated API、shade overlapping resource、plexus plugin 执行时的 SLF4J no provider warning。这些 warning 暂时不是当前迁移阻塞项。
+
+### 12.4 当前剩余注解范围
+
+`cat-home/src/main/java/com/dianping/cat/report/page` 已经清空 Unidal lookup 注解。
+
+`cat-home` 里剩余 `@Named` / `@Inject` 主要集中在:
+
+```text
+cat-home/src/main/java/com/dianping/cat/report/alert
+cat-home/src/main/java/com/dianping/cat/system/page
+cat-home/src/main/java/com/dianping/cat/report/task
+cat-home/src/main/java/com/dianping/cat/report/graph/metric/AbstractGraphCreator.java
+cat-home/src/main/java/com/dianping/cat/CatHomeModule.java
+```
+
+跨模块仍然存在 Unidal 注解和容器依赖,主要在 `cat-core`、`cat-consumer`、`cat-alarm`、`cat-hadoop`。
+
+因此现在还不能删除 Unidal / Plexus 依赖、`plexus-maven-plugin`、`codegen-maven-plugin` 或任何 `META-INF/plexus/components.xml`。
+
+### 12.5 当前完成度评估
+
+按最终目标“移除 Plexus / Unidal 相关依赖”估算,当前整体完成度约为:
+
+```text
+35% - 45%
+```
+
+判断依据:
+
+1. 报表页面服务层这一条主线已经基本清理完成。
+2. Spring 手工配置和旧 MVC 兼容桥接已经比较稳定。
+3. 但 alert、system page、core/consumer/alarm/hadoop、Unidal Web MVC、Unidal DAL/codegen、Plexus 插件链路仍未完成。
+4. `components.xml` 仍然需要生成,旧容器仍是兼容运行链路的一部分。
+
+### 12.6 推荐下一步计划
+
+下一步建议继续迁移 `cat-home/report/alert`,但要拆小批次,不要一次性迁移全部 alert 链路。
+
+推荐顺序:
+
+1. 先迁移低风险、已在 configurator 中注册或 Spring 中已有替代链路的 alert helper / manager。
+2. 再迁移 alert summary builder:`AlertSummaryExecutor`、`AlertSummaryService`、`RelatedSummaryBuilder`、`FailureSummaryBuilder`、`AlterationSummaryBuilder`、`AlertInfoBuilder`。
+3. 再迁移各类 alert domain:`business`、`transaction`、`event`、`heartbeat`、`exception`。
+4. 暂缓直接迁移 Web Handler、Unidal MVC、`RuleFTLDecorator` 等复杂链路。
+
+每一小批迁移后至少执行:
+
+```powershell
+mvn -pl cat-home -am -DskipTests compile
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+如果涉及 `components.xml` 生成,必须检查生成结果中是否包含预期 role、role-hint 和 field-name。
+
+### 12.7 重要注意事项
+
+1. 不要删除任何 `META-INF/plexus/components.xml`。
+2. 不要删除根 POM 或模块 POM 中的 Unidal / Plexus 依赖。
+3. 不要全量 Spring 扫描 `com.dianping.cat`。
+4. 不要让 Spring 和 Plexus 同时创建会启动线程、注册定时任务或注册 sync handler 的同一个组件。
+5. 迁移 descriptor 兼容组件时,优先使用显式 `field-name`,不要依赖注解扫描。
+6. 对 analyzer ID 相关 bean,避免 Spring bean 名和 analyzer ID 冲突,例如 `business`、`matrix`、`transaction` 等。
+7. 用户已经多次要求:如果验证没问题,可以继续后面的计划;但遇到需要运行时页面验证或业务决策的问题,应停下来让用户确认。
diff --git a/SPRING_CONTEXT_MIGRATION_PLAN.md b/SPRING_CONTEXT_MIGRATION_PLAN.md
new file mode 100644
index 0000000000..2420222964
--- /dev/null
+++ b/SPRING_CONTEXT_MIGRATION_PLAN.md
@@ -0,0 +1,1389 @@
+# Spring 上下文迁移详细执行计划
+
+## 1. 目标
+
+将 CAT 当前由 Plexus/Unidal Lookup 管理的应用上下文,逐步迁移到 Spring 管理。最终状态是:
+
+1. 应用启动入口由 Spring Boot 管理。
+2. 基础设施 Bean、数据源、MyBatis、事务由 Spring 管理。
+3. DAO、Repository、Service、Manager、后台任务、Web Controller 由 Spring 创建和装配。
+4. 新代码不再依赖 Plexus、Unidal Lookup、Unidal DAL 生成运行时。
+5. 最终移除 `plexus-maven-plugin`、`META-INF/plexus/components.xml` 生成链路、旧 Lookup 容器和旧 DAL 依赖。
+
+本计划采用分阶段迁移,不做一次性大替换。原因是当前 Plexus/Unidal 不只是 IoC 容器,还承担了组件生命周期、MVC 请求链路、DAL 代码生成、后台任务启动、日志注入等职责。一次性替换会把数据访问、页面渲染、JSP、Filter、定时任务和组件初始化问题混在一起,风险过高。
+
+推荐路线是:先让 Spring 接管边界清晰的基础设施和 DAO,再迁移 Service/Manager,随后迁移后台任务,最后处理 Web MVC 和旧容器清理。
+
+## 2. 当前状态
+
+已经完成的前置工作:
+
+1. 项目已升级到 JDK 21。
+2. 已新增 `cat-boot`,可以通过 Spring Boot 外壳启动内嵌 Tomcat。
+3. 日志组件已迁移到 Logback。
+4. JSON 组件已迁移到 fastjson2。
+5. 已引入 Lombok。
+6. `CatApplication.sql` 中的表已生成 MyBatis `DO`、`Mapper`、XML。
+7. 主要业务 DAO 已逐步切换到新的 MyBatis Repository 兼容层。
+8. 当前项目可以正常启动运行。
+
+当前仍然依赖旧框架的关键点:
+
+1. `cat-core`、`cat-home`、`cat-alarm`、`cat-consumer` 中仍大量使用 `org.unidal.lookup.annotation.Named` 和 `org.unidal.lookup.annotation.Inject`。
+2. 当前扫描结果显示,主代码中旧 Lookup 注解相关命中约 1145 处。
+3. `ContainerLoader`、`getDefaultContainer()`、`lookup(...)` 运行时查找命中约 33 处。
+4. `DataSourceManager` 命中约 67 处。
+5. `Initializable`、`LogEnabled`、`ContainerHolder` 生命周期相关命中约 224 处。
+6. 新 MyBatis Repository 当前约 50 个,但多数仍继承 `MyBatisRepositorySupport`。
+7. `MyBatisRepositorySupport` 当前仍通过 Unidal `DataSourceManager` 创建 MyBatis `SqlSessionFactory`。
+8. Web 请求仍主要经过 Unidal Web MVC、旧 Handler、Payload、Model、JSP Viewer 链路。
+
+这说明项目已经具备 Spring 化的基础,但旧容器仍然是主上下文。下一步应优先切断 DAO 基础设施对 Unidal 的依赖。
+
+## 3. 总体原则
+
+1. 每个阶段完成后都必须保持可编译、可打包、可启动。
+2. 每个阶段只迁移一个明确边界,避免 DAO、Service、Web、任务调度同时大改。
+3. 新增代码使用 Spring 标准方式:`@Configuration`、`@Bean`、`@Component`、`@Service`、`@Repository`、构造器注入、`@Transactional`。
+4. 新增 DAO/Repository 不再继承旧 DAO,也不依赖 Unidal DAL。
+5. 过渡期允许保留旧业务方法签名,例如 `DalException`、`DalNotFoundException`,用于降低调用方改动成本。
+6. `DalNotFoundException` 语义必须保持兼容。查询不到数据时不能被包装成普通 `DalException`,否则旧业务上层会误判为系统错误。
+7. Spring 和 Plexus 共存阶段必须明确组件归属。同一个组件不能同时由两个容器创建。
+8. 每迁移一类组件,就删除或停用对应的 Plexus role 注册,避免双容器重复初始化。
+9. 任何阶段发现运行时风险,都应回滚当前阶段,而不是跨阶段修补。
+
+## 4. 目标架构
+
+迁移完成后的目标链路:
+
+```text
+Spring Boot
+ -> Spring ApplicationContext
+ -> DataSource / TransactionManager
+ -> MyBatis SqlSessionFactory / Mapper
+ -> Repository
+ -> Service / Manager
+ -> Scheduler / Background Task
+ -> Spring MVC Controller / Filter
+```
+
+旧链路逐步退场:
+
+```text
+Plexus components.xml
+ -> Unidal Lookup
+ -> Unidal DAL DataSourceManager
+ -> Generated Dao
+ -> Unidal MVC Handler
+```
+
+## 5. 阶段 0:建立基线和保护网
+
+### 目标
+
+在正式迁移 Spring 上下文前,固定当前可运行状态,确保后续每一步都有可比较的基线。
+
+### 执行步骤
+
+1. 记录当前 Git 状态:
+
+```bash
+git status --short
+```
+
+2. 执行基础编译:
+
+```bash
+mvn -pl cat-core -am compile -DskipTests
+mvn -pl cat-alarm,cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+3. 启动应用:
+
+```bash
+java -jar cat-boot\target\cat-boot-4.0-RC1.jar
+```
+
+4. 验证默认启动参数是否生效:
+
+```text
+cat.home = C:\Users\Shang\.cat
+cat.log.path = C:\Users\Shang\.cat\logs
+```
+
+5. 访问关键页面:
+
+```text
+http://localhost:8080/cat
+http://localhost:8080/cat/r
+```
+
+6. 记录启动日志中的关键错误和警告,作为后续对比基线。
+
+### 验收标准
+
+1. 项目可以编译。
+2. `cat-boot` 可以打包。
+3. 应用可以启动。
+4. 页面可以访问。
+5. 日志可以输出到控制台和文件。
+
+### 回滚点
+
+此阶段不改代码,只建立基线。
+
+## 6. 阶段 1:建立 Spring 基础设施边界
+
+### 目标
+
+让 Spring 先管理基础设施 Bean,为后续替换 Plexus 提供入口。此阶段不替换 Unidal MVC,不改变页面请求链路。
+
+### 涉及模块
+
+1. `cat-boot`
+2. `cat-core`
+
+### 执行步骤
+
+1. 在 `cat-boot` 中建立 Spring 配置包:
+
+```text
+com.dianping.cat.boot.config
+com.dianping.cat.boot.mybatis
+com.dianping.cat.boot.bridge
+```
+
+2. 明确 Spring 扫描范围:
+
+```java
+@SpringBootApplication(scanBasePackages = {
+ "com.dianping.cat.boot"
+})
+```
+
+暂时不要全量扫描 `com.dianping.cat`,否则大量旧 `@Named` 组件可能被 Spring 误创建。
+
+3. 新增 Spring 上下文桥接类:
+
+```text
+SpringContextHolder
+```
+
+该类只用于过渡期。新增业务代码禁止通过静态 holder 获取 Bean。
+
+4. 新增启动健康检查 Bean:
+
+```text
+CatSpringStartupVerifier
+```
+
+检查内容:
+
+1. Spring `ApplicationContext` 已启动。
+2. 必要配置已加载。
+3. `cat.home` 和 `cat.log.path` 已初始化。
+
+5. 保持 `EmbeddedCatServer` 仍按现有方式部署 `cat-home.war` 到 `/cat`。
+
+### 验证方式
+
+```bash
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+java -jar cat-boot\target\cat-boot-4.0-RC1.jar
+```
+
+### 验收标准
+
+1. Spring Boot 正常启动。
+2. 内嵌 Tomcat 正常启动。
+3. `/cat` 应用正常部署。
+4. 没有重复 Bean、循环依赖、全量扫描导致的初始化异常。
+
+### 回滚点
+
+删除新增 Spring 配置和桥接类即可,不影响现有 Plexus 链路。
+
+## 7. 阶段 2:让 MyBatis 和数据源交给 Spring 管理
+
+### 目标
+
+切断新 MyBatis Repository 对 Unidal `DataSourceManager` 的依赖,让 `DataSource`、`SqlSessionFactory`、`SqlSessionTemplate`、事务全部由 Spring 管理。
+
+这是最推荐优先执行的阶段,边界清晰、收益最大。
+
+### 涉及模块
+
+1. `cat-boot`
+2. `cat-core`
+3. `cat-home`
+4. `cat-alarm`
+
+### 执行步骤
+
+1. 新增 Spring 数据源配置:
+
+```text
+com.dianping.cat.boot.mybatis.CatDataSourceConfiguration
+```
+
+短期策略:
+
+1. 优先复用当前 `datasources.xml` 或等价连接参数,降低配置变化。
+2. 用 Spring Bean 包装出标准 `javax.sql.DataSource`。
+3. 保持数据源名称 `cat` 的兼容语义。
+
+中期策略:
+
+1. 将数据库配置迁移到 `application.yml` 或外部配置文件。
+2. 使用 Spring Boot 标准 `spring.datasource.*`。
+3. 使用 HikariCP 或 Spring Boot 默认连接池。
+
+2. 新增 MyBatis 配置:
+
+```text
+com.dianping.cat.boot.mybatis.CatMyBatisConfiguration
+```
+
+配置内容:
+
+1. `SqlSessionFactoryBean`
+2. `SqlSessionTemplate`
+3. `DataSourceTransactionManager`
+4. `@MapperScan`
+5. Mapper XML 路径加载
+
+Mapper 扫描范围建议先限定在新包:
+
+```text
+com.dianping.cat.core.mybatis.mapper
+com.dianping.cat.home.mybatis.mapper
+com.dianping.cat.alarm.mybatis.mapper
+```
+
+3. 改造 `MyBatisRepositorySupport`。
+
+当前问题:
+
+1. 持有 Unidal `DataSourceManager`。
+2. 手工构造 `SqlSessionFactory`。
+3. Repository 自己管理 `SqlSession`。
+
+目标状态:
+
+1. 不再引用 `DataSourceManager`。
+2. 不再手工构造 `SqlSessionFactory`。
+3. Repository 通过构造器注入 Mapper。
+4. 写操作交给 Spring 事务。
+
+4. 先选择 3 个低风险 Repository 做试点:
+
+```text
+ConfigRepository
+DailyReportRepository
+HostinfoRepository
+```
+
+试点原因:
+
+1. `config` 是系统启动时高频读取表,能尽早暴露问题。
+2. `dailyreport` 已经迁移过,适合验证报表类查询。
+3. `hostinfo` 当前数据可能为空,适合验证空数据语义。
+
+5. 保持兼容方法签名。
+
+短期保留:
+
+```text
+DalException
+DalNotFoundException
+Readset
+Updateset
+```
+
+但方法内部不再依赖 Unidal DAL 查询引擎。
+
+6. 写操作增加事务边界:
+
+```java
+@Transactional
+```
+
+7. 删除已迁移 Repository 的 Plexus 注册。
+
+检查位置:
+
+```text
+cat-core/src/main/java/com/dianping/cat/build/*DatabaseConfigurator.java
+cat-home/src/main/java/com/dianping/cat/build/*DatabaseConfigurator.java
+cat-alarm/src/main/java/com/dianping/cat/build/*DatabaseConfigurator.java
+```
+
+### 验证方式
+
+编译验证:
+
+```bash
+mvn -pl cat-core -am compile -DskipTests
+mvn -pl cat-alarm,cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+运行验证:
+
+1. 启动应用。
+2. 访问配置页。
+3. 访问报表页。
+4. 检查启动阶段 `server-config` 查询。
+5. 检查 `hostinfo` 空表时是否符合旧行为。
+
+重点日志:
+
+1. 不能出现 Mapper 未注册。
+2. 不能出现事务管理器缺失。
+3. 不能出现 `DataSourceManager` 初始化失败。
+4. 查询不到数据时不能误报 ERROR。
+
+### 验收标准
+
+1. 试点 Repository 不再依赖 `DataSourceManager`。
+2. 试点 Repository 由 Spring 管理。
+3. Mapper 由 Spring 创建。
+4. 查询不到数据仍按旧行为抛 `DalNotFoundException`。
+5. 页面访问和启动配置加载正常。
+
+### 回滚点
+
+保留旧 `MyBatisRepositorySupport` 实现作为回滚参考。若 Spring MyBatis 配置异常,只回滚试点 Repository 和 Spring MyBatis 配置,不影响其他旧 DAO。
+
+## 8. 阶段 3:批量迁移全部 MyBatis Repository
+
+### 目标
+
+将当前约 50 个 MyBatis Repository 全部交给 Spring 管理,移除 Repository 层对 Unidal `DataSourceManager` 和 Plexus 注册的依赖。
+
+### 执行分组
+
+建议按模块分组迁移。
+
+第一组:`cat-core` 基础表。
+
+```text
+config
+dailyreport
+hourlyreport
+weeklyreport
+monthreport
+hostinfo
+project
+task
+business_config
+```
+
+第二组:`cat-home` 页面和配置表。
+
+```text
+alert_summary
+alteration
+baseline
+config_modification
+metric_graph
+metric_screen
+overload
+topology_graph
+```
+
+第三组:`cat-alarm` 告警表。
+
+```text
+alert
+server_alarm_rule
+user_define_rule
+```
+
+第四组:报表内容表。
+
+```text
+daily_report_content
+hourly_report_content
+monthly_report_content
+weekly_report_content
+```
+
+### 执行步骤
+
+1. 每次迁移一个模块或一组表,不跨模块混改。
+2. Repository 增加 Spring stereotype:
+
+```java
+@Repository
+```
+
+3. 使用构造器注入 Mapper。
+4. 删除 `extends MyBatisRepositorySupport`。
+5. 删除 `openSession()` 手动 session 管理。
+6. 删除对应 Plexus role 注册。
+7. 保持外部调用方法签名。
+8. 保持异常语义兼容。
+
+### 验证方式
+
+每迁移一组执行:
+
+```bash
+mvn -pl cat-core -am compile -DskipTests
+mvn -pl cat-home -am compile -DskipTests
+mvn -pl cat-alarm -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+检查旧依赖残留:
+
+```bash
+rg -n "extends MyBatisRepositorySupport|DataSourceManager" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java
+rg -n "C\\(.*Repository\\.class\\).*DataSourceManager" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java
+```
+
+### 验收标准
+
+1. 所有新 MyBatis Repository 不再继承 `MyBatisRepositorySupport`。
+2. Repository 层不再引用 `DataSourceManager`。
+3. Repository 层不再需要 Plexus 注册。
+4. 所有 Mapper 能由 Spring 注入。
+5. 常用页面和后台任务读写正常。
+
+### 回滚点
+
+按分组回滚。不要在一个提交中混合迁移全部 Repository。
+
+## 9. 阶段 4:迁移配置类 Manager 和基础 Service
+
+### 目标
+
+将配置管理类和基础业务服务从 Plexus 组件迁移为 Spring Bean。
+
+### 优先迁移对象
+
+第一批配置管理类:
+
+```text
+ServerConfigManager
+AlertConfigManager
+SenderConfigManager
+BusinessConfigManager
+AtomicMessageConfigManager
+TpValueStatisticConfigManager
+```
+
+第二批基础服务:
+
+```text
+HostinfoService
+ProjectService
+TaskManager
+ServerStatisticManager
+```
+
+### 执行步骤
+
+1. 将 `@Named` 替换为 Spring 注解:
+
+```java
+@Component
+@Service
+```
+
+2. 将 `org.unidal.lookup.annotation.Inject` 替换为构造器注入。
+3. 将 `Initializable.initialize()` 替换为:
+
+```java
+@PostConstruct
+```
+
+或在需要明确启动顺序时使用:
+
+```java
+SmartLifecycle
+ApplicationRunner
+```
+
+4. 将 `LogEnabled` 替换为 SLF4J:
+
+```java
+private static final Logger LOGGER = LoggerFactory.getLogger(CurrentClass.class);
+```
+
+5. 删除对应 Plexus role 注册。
+6. 对仍由 Plexus 创建、但需要访问 Spring Bean 的旧组件,短期使用桥接适配器。
+7. 避免新代码继续调用 `ContainerLoader.getDefaultContainer()`。
+
+### 验证方式
+
+```bash
+mvn -pl cat-core -am compile -DskipTests
+mvn -pl cat-alarm,cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+运行验证:
+
+1. 启动应用。
+2. 检查 `server-config` 加载。
+3. 修改配置后确认保存和读取正常。
+4. 检查配置刷新任务是否正常。
+5. 检查日志中没有重复初始化。
+
+### 验收标准
+
+1. 已迁移 Manager/Service 不再使用 Unidal `@Named`。
+2. 已迁移 Manager/Service 不再使用 Unidal `@Inject`。
+3. 已迁移 Manager/Service 不再依赖 `Initializable` 和 `LogEnabled`。
+4. 对应 Plexus role 已删除。
+5. 启动和配置读取行为保持兼容。
+
+### 回滚点
+
+按类回滚。配置管理类不要一次性全量迁移。
+
+## 10. 阶段 5:迁移报表 Service 和业务 Manager
+
+### 目标
+
+将报表查询、报表聚合、页面服务类从 Plexus 迁移到 Spring。
+
+### 迁移对象
+
+优先迁移服务类,而不是页面 Handler:
+
+```text
+LocalTransactionService
+HistoricalTransactionService
+TransactionReportService
+LocalEventService
+HistoricalEventService
+EventReportService
+LocalProblemService
+HistoricalProblemService
+ProblemReportService
+LocalHeartbeatService
+HistoricalHeartbeatService
+HeartbeatReportService
+```
+
+随后迁移:
+
+```text
+DefaultReportManager
+AbstractReportService 子类
+BusinessReportService
+DependencyReportService
+StateReportService
+StorageReportService
+MatrixReportService
+TopReportService
+```
+
+### 执行步骤
+
+1. 每次选择一个报表域迁移,例如 transaction 或 event。
+2. 将该报表域 Service 改为 Spring Bean。
+3. 保持 Handler 暂时仍由旧 MVC 创建。
+4. 通过桥接方式让旧 Handler 获取 Spring Service。
+5. 验证页面结果后,再迁移下一个报表域。
+
+### 验证页面
+
+至少覆盖:
+
+```text
+/cat/r/t
+/cat/r/e
+/cat/r/p
+/cat/r/h
+/cat/r/state
+/cat/r/top
+```
+
+具体 URL 以当前路由为准。
+
+### 验收标准
+
+1. 已迁移报表 Service 由 Spring 创建。
+2. 旧 Handler 仍可正常调用这些 Service。
+3. 报表页面正常渲染。
+4. 查询历史报表和实时报表均正常。
+
+### 回滚点
+
+按报表域回滚。某个报表域失败,不影响其他报表域。
+
+## 11. 阶段 6:迁移后台任务和调度
+
+### 目标
+
+用 Spring 生命周期和 Spring 调度机制管理后台任务,替代 Plexus 初始化期间启动任务的方式。
+
+### 迁移对象
+
+```text
+TimerSyncTask
+ReportReloadTask
+DefaultTaskConsumer
+ProjectUpdateTask
+CurrentReportBuilder
+各类 ReportBuilder
+各类 ReportReloader
+CapacityUpdateTask
+```
+
+### 执行步骤
+
+1. 新增 Spring 调度配置:
+
+```java
+@EnableScheduling
+```
+
+2. 配置统一线程池:
+
+```text
+ThreadPoolTaskScheduler
+```
+
+3. 将手动启动线程替换为:
+
+```java
+@Scheduled
+SmartLifecycle
+ApplicationRunner
+```
+
+4. 每个任务增加开关配置,支持临时关闭。
+5. 任务启动日志必须包含任务名称和调度周期。
+6. 应用关闭时任务必须优雅停止。
+7. 防止同一任务被 Plexus 和 Spring 重复启动。
+
+### 验证方式
+
+1. 启动应用,观察任务启动日志。
+2. 检查同一任务是否只启动一次。
+3. 触发报表 reload。
+4. 触发项目更新任务。
+5. 关闭应用,确认线程池正常退出。
+
+### 验收标准
+
+1. 已迁移任务由 Spring 生命周期管理。
+2. 任务不会重复启动。
+3. 应用关闭无挂起线程。
+4. 任务异常有统一日志。
+
+### 回滚点
+
+按任务回滚。先迁移低风险任务,再迁移核心报表生成任务。
+
+## 12. 阶段 7:迁移 Web 层到 Spring MVC
+
+### 目标
+
+用 Spring MVC 替换 Unidal Web MVC 请求生命周期。
+
+这是风险最高、改动最大的阶段,应在 DAO、Service、Manager、任务基本迁移完成后再执行。
+
+### 当前旧链路
+
+```text
+org.unidal.web.MVC
+DefaultRequestLifecycle
+Page Handler
+Payload
+Model
+BaseJspViewer
+JSP
+```
+
+### 迁移策略
+
+优先采用页面级渐进迁移:
+
+1. 第一阶段:Spring Controller 包装旧 Handler。
+2. 第二阶段:逐个页面替换为 Spring MVC Controller。
+3. 第三阶段:替换 Payload/Model 绑定方式。
+4. 第四阶段:替换 JSP Viewer。
+5. 第五阶段:删除 Unidal MVC。
+
+### 优先页面
+
+先迁移低风险页面:
+
+```text
+home
+config
+state
+logview
+```
+
+再迁移核心报表页面:
+
+```text
+transaction
+event
+problem
+heartbeat
+business
+dependency
+matrix
+top
+```
+
+最后迁移告警和复杂配置页面:
+
+```text
+alert
+router config
+overload
+storage
+statistics
+```
+
+### 执行步骤
+
+1. 新增 Spring MVC 配置。
+2. 配置 JSP ViewResolver。
+3. 保持静态资源路径兼容。
+4. 迁移 Filter:
+
+```text
+PermissionFilter
+DomainFilter
+CatFilter
+```
+
+5. 增加统一异常处理:
+
+```java
+@ControllerAdvice
+```
+
+6. 逐个页面迁移 Controller。
+7. 保持原 URL 尽量不变。
+8. 每迁移一个页面就删除对应旧路由注册。
+
+### 验证页面
+
+至少覆盖:
+
+```text
+http://localhost:8080/cat
+http://localhost:8080/cat/r
+http://localhost:8080/cat/r/t
+http://localhost:8080/cat/r/e
+http://localhost:8080/cat/r/p
+配置页面
+告警页面
+报表历史查询页面
+```
+
+### 验收标准
+
+1. 页面可以访问。
+2. JSP 正常渲染。
+3. 静态资源正常加载。
+4. 权限、domain、跳转逻辑正常。
+5. 原有 URL 尽量兼容。
+6. 已迁移页面不再经过 Unidal MVC。
+
+### 回滚点
+
+按页面回滚。不要一次性替换全部 MVC。
+
+## 13. 阶段 8:移除 Plexus/Unidal Lookup
+
+### 目标
+
+彻底移除旧容器。
+
+### 前置条件
+
+必须同时满足:
+
+1. DAO 已全部由 Spring 管理。
+2. Repository 不再依赖 `DataSourceManager`。
+3. Service/Manager 已由 Spring 管理。
+4. 后台任务已由 Spring 管理。
+5. Web 层已迁移到 Spring MVC,或不再依赖 Unidal MVC 生命周期。
+6. 运行时代码不再调用 `ContainerLoader.getDefaultContainer()`。
+7. 构建不再需要生成 `components.xml`。
+
+### 执行步骤
+
+1. 删除 `plexus-maven-plugin`。
+2. 删除 `META-INF/plexus/components.xml` 生成流程。
+3. 删除旧组件注册类。
+4. 删除 `org.unidal.lookup` 运行时依赖。
+5. 删除 `ContainerHolder`、`ContainerLoader` 使用。
+6. 删除旧 DAL 生成流程:
+
+```text
+codegen-maven-plugin dal-jdbc
+Readset
+Updateset
+旧 *Dao
+```
+
+7. 清理 POM 依赖:
+
+```text
+org.unidal.framework:dal-jdbc
+org.unidal.framework:foundation-service
+org.unidal.framework:web-framework
+org.unidal.framework:test-framework
+```
+
+注意:只有当没有代码引用时才能删除。
+
+### 验证方式
+
+```bash
+mvn clean package -DskipTests
+mvn test
+```
+
+运行验证:
+
+1. 应用启动。
+2. 页面访问。
+3. 数据库读写。
+4. 客户端上报。
+5. 报表生成。
+6. 告警链路。
+7. 应用关闭。
+
+### 验收标准
+
+1. 依赖树中不再需要 Plexus/Unidal Lookup。
+2. 构建不再生成 `components.xml`。
+3. 运行时没有 Plexus 容器初始化日志。
+4. 核心功能仍可用。
+
+### 回滚点
+
+此阶段风险最高,只能在前面阶段全部完成并稳定后执行。建议单独提交,单独验证。
+
+## 14. 每阶段通用检查清单
+
+每完成一个阶段或一组类迁移,都执行以下检查。
+
+### 编译和打包
+
+```bash
+mvn -pl cat-core -am compile -DskipTests
+mvn -pl cat-alarm,cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+### 旧 DAO import 检查
+
+```bash
+rg -n "import com\.dianping\.cat\.(core\.dal|core\.config|home\.dal\.report|alarm)\.[A-Za-z0-9]+Dao;" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+```
+
+### 旧容器查找检查
+
+```bash
+rg -n "ContainerLoader|getDefaultContainer|lookup\(" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+```
+
+### Plexus 注解和生命周期检查
+
+```bash
+rg -n "org\.unidal\.lookup\.annotation|@Named|Initializable|LogEnabled|ContainerHolder" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+```
+
+### 数据源旧依赖检查
+
+```bash
+rg -n "DataSourceManager|MyBatisRepositorySupport" cat-core/src/main/java cat-home/src/main/java cat-alarm/src/main/java cat-consumer/src/main/java
+```
+
+### 运行验证
+
+1. 启动应用。
+2. 访问首页。
+3. 访问报表页。
+4. 访问配置页。
+5. 修改并保存一项配置。
+6. 检查日志文件。
+7. 检查控制台 ERROR。
+8. 正常停止应用。
+
+## 15. 风险和应对
+
+### 风险 1:双容器重复创建组件
+
+表现:
+
+1. 初始化执行两次。
+2. 后台任务重复启动。
+3. 配置重复加载。
+
+应对:
+
+1. Spring 扫描范围保持收敛。
+2. 每迁移一个组件就删除对应 Plexus role。
+3. 后台任务迁移时增加启动日志和唯一性检查。
+
+### 风险 2:异常语义不兼容
+
+表现:
+
+1. 查询不到数据被记录为 ERROR。
+2. 旧业务上层逻辑进入异常分支。
+
+应对:
+
+1. `DalNotFoundException` 必须原样透传。
+2. Repository 不要统一包装所有异常。
+3. 为核心查询补充单元测试。
+
+### 风险 3:事务边界变化
+
+表现:
+
+1. 写操作未提交。
+2. 多表更新部分成功。
+3. 连接泄漏。
+
+应对:
+
+1. 写操作统一加 `@Transactional`。
+2. Repository 不再手动管理 session。
+3. 用 Spring `DataSourceTransactionManager` 管理事务。
+
+### 风险 4:启动顺序变化
+
+表现:
+
+1. 配置还未加载,任务已启动。
+2. Web 页面访问时 Service 未初始化。
+
+应对:
+
+1. 对关键 Bean 使用明确生命周期。
+2. 后台任务使用 `SmartLifecycle` 或 `ApplicationReadyEvent`。
+3. 不依赖字段注入的隐式顺序。
+
+### 风险 5:Web 层迁移破坏 URL 和 JSP
+
+表现:
+
+1. 原 URL 404。
+2. JSP tag 或静态资源缺失。
+3. 权限过滤逻辑失效。
+
+应对:
+
+1. Web 层最后迁移。
+2. 按页面迁移。
+3. 保留原 URL。
+4. 每个页面迁移后单独验收。
+
+## 16. 推荐下一步
+
+建议下一步从阶段 2 开始,先做一个最小可验证闭环:
+
+1. 新增 Spring `DataSource`、`SqlSessionFactory`、`SqlSessionTemplate`、`TransactionManager` 配置。
+2. 只迁移 `ConfigRepository`、`DailyReportRepository`、`HostinfoRepository` 三个 Repository。
+3. 保持旧方法签名和异常语义。
+4. 删除这三个 Repository 对 `DataSourceManager` 的依赖。
+5. 启动应用验证 `server-config`、报表查询、空 `hostinfo` 表行为。
+6. 通过后再批量迁移其他 Repository。
+
+这个顺序收益最大,因为它先把 DAO 基础设施从 Unidal 中解耦出来,同时不会立即触碰最高风险的 Web MVC 链路。
+
+## 17. 2026-06-09 当前结论与下一步计划
+
+### 当前代码基线
+
+用户已提交上一轮日志补充和 Spring 迁移相关代码。以 2026-06-09 当前工作树为新基线,`git status --short` 为空。
+
+当前项目仍不能直接删除 Plexus / Unidal 依赖。原因是旧框架仍承担三类核心职责:
+
+1. 组件容器和运行时查找:仍有约 27 个生产 Java 文件使用 `ContainerHolder`、`lookup(...)`、`lookupMap(...)`。
+2. Unidal Web MVC:`cat-home` 中约 146 个生产 Java 文件仍涉及 `org.unidal.web`、`PageHandler`、`ActionPayload`、`ViewModel`、`BaseJspViewer`、`FieldMeta` 等。
+3. Unidal DAL / codegen:约 154 个生产 Java 文件仍涉及 `org.unidal.dal`、`DalException`、`DalNotFoundException` 或生成 DAO / model。
+
+根 `pom.xml` 中仍存在以下关键依赖或插件管理项:
+
+```text
+org.unidal.framework:dal-jdbc
+org.unidal.framework:foundation-service
+org.unidal.framework:web-framework
+org.unidal.framework:test-framework
+org.unidal.webres:WebResServer
+org.unidal.maven.plugins:codegen-maven-plugin
+org.unidal.maven.plugins:plexus-maven-plugin
+```
+
+源码资源中仍存在 Plexus 组件描述文件:
+
+```text
+cat-alarm/src/main/resources/META-INF/plexus/components.xml
+cat-consumer/src/main/resources/META-INF/plexus/components.xml
+cat-core/src/main/resources/META-INF/plexus/components.xml
+cat-hadoop/src/main/resources/META-INF/plexus/components.xml
+cat-home/src/main/resources/META-INF/plexus/components.xml
+```
+
+这些文件暂时不能提前删除,必须等对应 Spring 替代链路验证通过后再分批移除。
+
+### 已存在的迁移桥接基础
+
+当前已经具备继续迁移的 Spring 桥接基础:
+
+1. `cat-home/src/main/java/com/dianping/cat/home/spring/CatHomeSpringContextListener.java` 已创建 Spring `AnnotationConfigApplicationContext`,并将上下文写入 `ServletContext` 和 `CatSpringContext`。
+2. `cat-home/src/main/java/com/dianping/cat/home/spring/CatHomeSpringConfiguration.java` 已手动注册大量 Repository、ConfigManager、Service Bean。
+3. `cat-core/src/main/java/com/dianping/cat/spring/CatSpringContext.java` 已提供旧代码获取 Spring Bean 的过渡桥。
+4. `cat-boot/src/main/java/com/dianping/cat/boot/CatBootApplication.java` 已作为 Spring Boot 启动入口。
+
+但当前 `cat-home/src/main/webapp/WEB-INF/web.xml` 仍将 `/r/*` 和 `/s/*` 交给 `org.unidal.web.MVC`,`CatServlet` 也仍依赖 `AbstractContainerServlet`、`DefaultModuleContext`、`ModuleInitializer`。因此 Web MVC 和启动模块初始化仍属于高风险区域,暂不作为下一步优先项。
+
+### 迁移顺序调整
+
+截至 2026-06-09,推荐迁移顺序调整为:
+
+1. 先替换组件容器和 `lookupMap(...)` 型扩展点。
+2. 再移除 Plexus 生命周期和日志接口,例如 `Initializable`、`LogEnabled`、`org.codehaus.plexus.logging.Logger`。
+3. 然后迁移 Unidal Web MVC。
+4. 再继续替换 Unidal DAL / codegen。
+5. 最后移除 `plexus-maven-plugin`、`codegen-maven-plugin`、`components.xml` 和根 POM 中的 Unidal 依赖。
+
+此顺序的原因是:如果先迁移 Web MVC 或 DAL,会同时牵动 URL 路由、JSP、Filter、Repository、异常语义和启动流程;而 `lookupMap(...)` 型 Manager 边界较清晰,可以先通过 Spring `Map` / `List` 注入建立替代链路,并保留旧 Plexus fallback 作为回滚保护。
+
+### 下一步执行项
+
+下一步建议迁移第一组低风险 `lookupMap(...)` 管理器:
+
+```text
+cat-alarm/src/main/java/com/dianping/cat/alarm/spi/sender/SenderManager.java
+cat-alarm/src/main/java/com/dianping/cat/alarm/spi/decorator/DecoratorManager.java
+cat-alarm/src/main/java/com/dianping/cat/alarm/spi/spliter/SpliterManager.java
+```
+
+执行方式:
+
+1. 为 Manager 增加 Spring 注入入口,例如 setter 或构造器注入。
+2. 优先使用 Spring 注入的 `Map`、`Map`、`Map`。
+3. 暂时保留原有 `lookupMap(...)` fallback,确保 Spring Bean 未完全注册时旧链路仍可运行。
+4. 在 `CatHomeSpringConfiguration` 中注册 Manager 及其扩展点 Bean。
+5. 增加必要的 `info` / `warn` 日志,明确当前使用的是 Spring 注入还是 Plexus fallback。
+6. 编译并启动验证告警相关初始化、配置页和常用 `/cat/r/*` 页面。
+
+### 验证命令
+
+```powershell
+mvn -pl cat-alarm,cat-home -am compile -DskipTests
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+java -Dserver.port=18080 -Dcat.tcp.port=12280 -jar cat-boot\target\cat-boot-4.0-RC1.jar
+```
+
+建议验证 URL:
+
+```text
+http://127.0.0.1:18080/cat
+http://127.0.0.1:18080/cat/r
+http://127.0.0.1:18080/cat/r/business
+http://127.0.0.1:18080/cat/s/config
+```
+
+验收标准:
+
+1. 编译和打包通过。
+2. 应用能启动,且没有重复初始化同一 Manager。
+3. 告警 SPI Manager 能正常加载扩展点。
+4. 日志能看出 Spring 注入是否生效。
+5. 如果 Spring 注入缺失,应有明确 warn,并能回退到 Plexus `lookupMap(...)`。
+6. `/cat/r/business` 和配置页保持可访问。
+
+### 暂缓事项
+
+以下事项暂缓,不进入下一步改动:
+
+1. 暂不修复用户当前环境中没有复现的 `/cat/r/business` NPE。
+2. 暂不删除任何 `META-INF/plexus/components.xml`。
+3. 暂不移除根 POM 中的 Unidal / Plexus 依赖。
+4. 暂不迁移 `org.unidal.web.MVC`。
+5. 暂不批量替换所有 DAL / codegen 相关代码。
+
+## 18. 2026-06-13 当前进度更新
+
+### 已完成
+
+最近一批迁移已经完成并提交,重点是把 `cat-home` 报表页面服务层从 Unidal lookup 注解迁移到显式注册:
+
+```text
+cat-home/src/main/java/com/dianping/cat/report/page
+```
+
+该目录下当前已经没有 `@Named` / `@Inject` 残留。
+
+已覆盖的报表域:
+
+```text
+transaction
+event
+problem
+heartbeat
+storage
+cross
+matrix
+state
+top
+business
+metric baseline
+```
+
+同时已完成:
+
+```text
+report/graph/svg -> GraphBuilder / ValueTranslater 显式注册
+report/graph/metric/impl/DataExtractorImpl -> DataExtractor 显式注册
+report/task/reload -> 清理 Unidal 注解,保留 Spring setter 注入路径
+```
+
+### 验证结果
+
+最近一批迁移后已通过:
+
+```powershell
+mvn -pl cat-home -am -DskipTests compile
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+## 19. 终极目标:移除 Unidal / Plexus 相关依赖的后续路线
+
+本节记录 2026-06-14 之后的后续操作计划。当前 `@Named` / `@Inject` lookup 注解已经基本清空,但项目距离彻底移除 Unidal / Plexus 依赖仍有明显距离。后续重点需要从“注解迁移”切换到“依赖拆解”。
+
+### 19.1 当前依赖面分层
+
+当前剩余依赖大致分为四层:
+
+1. 构建期依赖
+
+```text
+org.unidal.maven.plugins:codegen-maven-plugin
+org.unidal.maven.plugins:plexus-maven-plugin
+```
+
+这两类插件仍负责生成 DAL model、DAL JDBC 代码,以及 `META-INF/plexus/components.xml`。在 DAL、MVC、运行时容器完全替换前,不应直接删除。
+
+2. 运行时容器和 descriptor
+
+```text
+META-INF/plexus/components.xml
+org.unidal.lookup.*
+org.codehaus.plexus.*
+```
+
+这部分仍承载旧容器的组件注册、`lookup(...)`、`lookupMap(...)`、生命周期初始化和 fallback 链路。当前不能一次性删除。
+
+3. Web MVC 依赖
+
+```text
+org.unidal.web.mvc.*
+org.unidal.web.MVC
+BaseJspViewer
+PageHandler
+ActionContext
+PayloadMeta / ModelMeta / FieldMeta
+```
+
+这部分影响 `/cat/r/*`、`/cat/s/*` 路由、页面 handler、payload、model、JSP viewer 和 servlet 初始化链路,属于高风险迁移项,应放在中后期。
+
+4. 工具类 / 辅助类 / 测试基建
+
+```text
+org.unidal.helper.*
+org.unidal.tuple.Pair
+org.unidal.lookup.util.StringUtils
+org.codehaus.plexus.util.StringUtils
+org.unidal.lookup.ComponentTestCase
+lookup(...) in tests
+```
+
+这部分风险相对低,适合作为下一阶段的入口,用小批量替换逐步缩小 Unidal 依赖面。
+
+### 19.2 推荐拆除顺序
+
+后续建议按以下顺序推进:
+
+1. 建立依赖清单和防回归检查。
+2. 替换低风险工具类和辅助类。
+3. 清理测试中的 `ComponentTestCase` / `lookup(...)`。
+4. 迁移剩余 `lookupMap(...)` 扩展点管理器。
+5. 替换或隔离 Unidal Web MVC。
+6. 替换或隔离 Unidal DAL / codegen。
+7. 移除 `plexus-maven-plugin` 和 `components.xml` 生成链路。
+8. 最后移除根 POM 中 Unidal / Plexus 相关依赖和仓库配置。
+
+### 19.3 下一步具体执行项
+
+下一步不建议直接删除 POM 依赖,而是先做“依赖清单 + 第一批低风险替换”。
+
+执行项如下:
+
+1. 生成按模块统计的源码引用清单:
+
+```powershell
+rg -n "org\.unidal\.lookup|org\.unidal\.web\.mvc|org\.unidal\.dal\.jdbc|org\.unidal\.helper|org\.unidal\.tuple|org\.codehaus\.plexus" --glob "**/src/**/*.{java,scala,xml}" .
+```
+
+输出需要按模块分组,区分:
+
+```text
+cat-client
+cat-core
+cat-consumer
+cat-hadoop
+cat-alarm
+cat-home
+cat-boot
+integration
+```
+
+2. 先替换低风险工具类:
+
+```text
+org.codehaus.plexus.util.StringUtils
+org.unidal.lookup.util.StringUtils
+org.unidal.tuple.Pair
+org.unidal.helper.Files
+org.unidal.helper.Splitters
+org.unidal.helper.Urls
+```
+
+替换原则:
+
+```text
+StringUtils -> JDK / Spring / Apache Commons Lang 中已存在能力
+Pair -> Java record / Map.Entry / Spring Pair / 局部小对象
+Files -> java.nio.file.Files
+Urls -> java.net.URI / URL / URLEncoder / UriComponentsBuilder
+Splitters -> JDK String split / Pattern / Stream
+```
+
+每一批只替换一个工具类族,避免同时改动业务语义和依赖边界。
+
+3. 优先选择不影响启动链路的目录:
+
+```text
+cat-home/src/main/java/com/dianping/cat/report/page/*/transform
+cat-home/src/main/java/com/dianping/cat/report/page/*/graph
+cat-home/src/main/java/com/dianping/cat/report/page/*/display
+cat-consumer/src/test/java
+cat-core/src/test/java
+```
+
+暂缓目录:
+
+```text
+cat-home/src/main/java/com/dianping/cat/*/Handler.java
+cat-home/src/main/java/com/dianping/cat/*/Payload.java
+cat-home/src/main/java/com/dianping/cat/*/Model.java
+cat-home/src/main/java/com/dianping/cat/*/JspViewer.java
+CatServlet / CatHomeModule / Web MVC 初始化链路
+DAL repository / DAO / codegen 生成类
+```
+
+4. 替换后执行精确扫描:
+
+```powershell
+rg -n "org\.unidal\.lookup\.util\.StringUtils|org\.codehaus\.plexus\.util\.StringUtils|org\.unidal\.tuple\.Pair|org\.unidal\.helper\.(Files|Splitters|Urls)" --glob "**/src/**/*.{java,scala}" .
+```
+
+5. 执行 Maven 验证:
+
+```powershell
+mvn validate
+mvn -pl cat-home -am -DskipTests compile
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
+
+如果只改测试代码,则至少执行:
+
+```powershell
+mvn -pl cat-consumer,cat-home -am -DskipTests test-compile
+```
+
+### 19.4 暂时不要做的事
+
+在完成工具类、测试基建、`lookupMap(...)` 和 Web MVC 替换前,暂时不要执行以下操作:
+
+1. 不要删除根 POM 中的 `org.unidal.framework` 依赖。
+2. 不要删除 `org.unidal.maven.plugins:codegen-maven-plugin`。
+3. 不要删除 `org.unidal.maven.plugins:plexus-maven-plugin`。
+4. 不要删除任何 `META-INF/plexus/components.xml`。
+5. 不要全量替换 `org.unidal.web.mvc.*`。
+6. 不要一次性重写 DAL / DAO / Repository 层。
+7. 不要因为看到第三方 `@Inject` 就全局禁用 `@Inject`;当前只应禁止 `org.unidal.lookup.annotation.Inject` 和 `org.unidal.lookup.annotation.Named`。
+
+### 19.5 当前已加入的防回归检查
+
+已在父 `pom.xml` 中加入 validate 阶段检查,禁止源码重新引入:
+
+```text
+import org.unidal.lookup.annotation.Inject;
+import org.unidal.lookup.annotation.Named;
+```
+
+该检查不会误伤以下第三方注入:
+
+```text
+org.elasticsearch.common.inject.Inject
+javax.inject.Inject
+```
+
+后续可以逐步扩展防回归规则,但每次只能禁止已经完成替换并验证通过的依赖族。
+
+### 当前阶段判断
+
+项目仍然不能直接移除 Plexus / Unidal 依赖。当前大致完成度约为:
+
+```text
+35% - 45%
+```
+
+主要剩余工作:
+
+```text
+cat-home/report/alert
+cat-home/system/page
+cat-home/report/task
+cat-core
+cat-consumer
+cat-alarm
+cat-hadoop
+Unidal Web MVC
+Unidal DAL / codegen
+Plexus components.xml 生成链路
+```
+
+### 下一步推荐
+
+下一步建议进入 `cat-home/report/alert` 的小批量迁移,优先迁移低风险 helper / manager / summary builder,不要直接迁移 Web Handler 或删除 `components.xml`。
+
+推荐顺序:
+
+```text
+1. Alert helper / config manager
+2. Alert summary service / executor / builders
+3. business / transaction / event / heartbeat / exception alert domain
+4. 最后再考虑 system/page 和 Web MVC
+```
+
+每完成一小批后继续执行:
+
+```powershell
+mvn -pl cat-home -am -DskipTests compile
+mvn -pl cat-boot -am package -DskipTests "-Dmaven.javadoc.skip=true"
+```
diff --git a/cat-alarm/pom.xml b/cat-alarm/pom.xml
index 0d7c8058ce..5881e80bff 100644
--- a/cat-alarm/pom.xml
+++ b/cat-alarm/pom.xml
@@ -20,16 +20,12 @@
cat-core
- org.unidal.framework
- foundation-service
+ org.mybatis
+ mybatis
- org.unidal.framework
- web-framework
-
-
- org.unidal.framework
- dal-jdbc
+ org.projectlombok
+ lombok
javax.servlet
@@ -46,15 +42,14 @@
org.freemarker
freemarker
-
- org.unidal.framework
- test-framework
- test
-
commons-lang
commons-lang
+
+ org.apache.commons
+ commons-lang3
+
com.google.code.gson
gson
@@ -72,8 +67,8 @@
httpmime
- com.alibaba
- fastjson
+ com.alibaba.fastjson2
+ fastjson2
junit
@@ -81,57 +76,6 @@
test
-
-
-
- org.unidal.maven.plugins
- codegen-maven-plugin
-
-
- generate data model
- generate-sources
-
- dal-model
-
-
- ${basedir}/src/main/resources/META-INF/dal/model/server-alarm-rule-manifest.xml,
- ${basedir}/src/main/resources/META-INF/dal/model/sender-config-manifest.xml,
- ${basedir}/src/main/resources/META-INF/dal/model/alert-policy-manifest.xml,
- ${basedir}/src/main/resources/META-INF/dal/model/monitor-rules-manifest.xml,
- ${basedir}/src/main/resources/META-INF/dal/model/alert-receiver-manifest.xml,
-
-
-
-
- generate dal jdbc model
- generate-sources
-
- dal-jdbc
-
-
- ${basedir}/src/main/resources/META-INF/dal/jdbc/alarm-manifest.xml,
-
-
-
-
-
- org.unidal.maven.plugins
- plexus-maven-plugin
-
-
- generate plexus component descriptor
- process-classes
-
- plexus
-
-
- com.dianping.cat.build.ComponentsConfigurator
-
-
-
-
-
-
utf-8
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/Alert.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/Alert.java
new file mode 100644
index 0000000000..23cfb03443
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/Alert.java
@@ -0,0 +1,183 @@
+package com.dianping.cat.alarm;
+
+
+public class Alert {
+ private long m_id;
+
+ private String m_domain;
+
+ private java.util.Date m_alertTime;
+
+ private String m_category;
+
+ private String m_type;
+
+ private String m_content;
+
+ private String m_metric;
+
+ private java.util.Date m_createTime;
+
+ private java.util.Date m_updateTime;
+
+ private long m_keyId;
+
+ private java.util.Date m_startTime;
+
+ private java.util.Date m_endTime;
+
+ private String[] m_categories;
+ public void afterLoad() {
+ m_keyId = m_id;
+ }
+
+ public java.util.Date getAlertTime() {
+ return m_alertTime;
+ }
+
+ public String[] getCategories() {
+ return m_categories;
+ }
+
+ public String getCategory() {
+ return m_category;
+ }
+
+ public String getContent() {
+ return m_content;
+ }
+
+ public java.util.Date getCreationDate() {
+ return m_createTime;
+ }
+
+ public java.util.Date getCreateTime() {
+ return m_createTime;
+ }
+
+ public String getDomain() {
+ return m_domain;
+ }
+
+ public java.util.Date getEndTime() {
+ return m_endTime;
+ }
+
+ public long getId() {
+ return m_id;
+ }
+
+ public long getKeyId() {
+ return m_keyId;
+ }
+
+ public String getMetric() {
+ return m_metric;
+ }
+
+ public java.util.Date getStartTime() {
+ return m_startTime;
+ }
+
+ public String getType() {
+ return m_type;
+ }
+
+ public java.util.Date getUpdateTime() {
+ return m_updateTime;
+ }
+
+ public Alert setAlertTime(java.util.Date alertTime) {
+ m_alertTime = alertTime;
+ return this;
+ }
+
+ public Alert setCategories(String[] categories) {
+ m_categories = categories;
+ return this;
+ }
+
+ public Alert setCategory(String category) {
+ m_category = category;
+ return this;
+ }
+
+ public Alert setContent(String content) {
+ m_content = content;
+ return this;
+ }
+
+ public Alert setCreationDate(java.util.Date creationDate) {
+ m_createTime = creationDate;
+ return this;
+ }
+
+ public Alert setCreateTime(java.util.Date createTime) {
+ m_createTime = createTime;
+ return this;
+ }
+
+ public Alert setDomain(String domain) {
+ m_domain = domain;
+ return this;
+ }
+
+ public Alert setEndTime(java.util.Date endTime) {
+ m_endTime = endTime;
+ return this;
+ }
+
+ public Alert setId(long id) {
+ m_id = id;
+ m_keyId = id;
+ return this;
+ }
+
+ public Alert setKeyId(long keyId) {
+ m_keyId = keyId;
+ return this;
+ }
+
+ public Alert setMetric(String metric) {
+ m_metric = metric;
+ return this;
+ }
+
+ public Alert setStartTime(java.util.Date startTime) {
+ m_startTime = startTime;
+ return this;
+ }
+
+ public Alert setType(String type) {
+ m_type = type;
+ return this;
+ }
+
+ public Alert setUpdateTime(java.util.Date updateTime) {
+ m_updateTime = updateTime;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(1024);
+
+ sb.append("Alert[");
+ sb.append("alert-time: ").append(m_alertTime);
+ sb.append(", categories: ").append(m_categories == null ? null : java.util.Arrays.asList(m_categories));
+ sb.append(", category: ").append(m_category);
+ sb.append(", content: ").append(m_content);
+ sb.append(", create-time: ").append(m_createTime);
+ sb.append(", domain: ").append(m_domain);
+ sb.append(", end-time: ").append(m_endTime);
+ sb.append(", id: ").append(m_id);
+ sb.append(", key-id: ").append(m_keyId);
+ sb.append(", metric: ").append(m_metric);
+ sb.append(", start-time: ").append(m_startTime);
+ sb.append(", type: ").append(m_type);
+ sb.append(", update-time: ").append(m_updateTime);
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/ServerAlarmRule.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/ServerAlarmRule.java
new file mode 100644
index 0000000000..ee25153dc0
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/ServerAlarmRule.java
@@ -0,0 +1,179 @@
+package com.dianping.cat.alarm;
+
+
+public class ServerAlarmRule {
+ private long m_id;
+
+ private String m_category;
+
+ private String m_endPoint;
+
+ private String m_measurement;
+
+ private String m_tags;
+
+ private String m_content;
+
+ private String m_type;
+
+ private String m_creator;
+
+ private java.util.Date m_createTime;
+
+ private java.util.Date m_updateTime;
+
+ private long m_keyId;
+ public void afterLoad() {
+ m_keyId = m_id;
+ }
+
+ public String getCategory() {
+ return m_category;
+ }
+
+ public String getContent() {
+ return m_content;
+ }
+
+ public java.util.Date getCreationDate() {
+ return m_createTime;
+ }
+
+ public java.util.Date getCreateTime() {
+ return m_createTime;
+ }
+
+ public String getCreator() {
+ return m_creator;
+ }
+
+ public String getEndPoint() {
+ return m_endPoint;
+ }
+
+ public long getId() {
+ return m_id;
+ }
+
+ public long getKeyId() {
+ return m_keyId;
+ }
+
+ public String getMeasurement() {
+ return m_measurement;
+ }
+
+ public String getTags() {
+ return m_tags;
+ }
+
+ public String getType() {
+ return m_type;
+ }
+
+ public java.util.Date getUpdatetime() {
+ return m_updateTime;
+ }
+
+ public java.util.Date getUpdateTime() {
+ return m_updateTime;
+ }
+
+ public ServerAlarmRule setCategory(String category) {
+ m_category = category;
+ return this;
+ }
+
+ public ServerAlarmRule setContent(String content) {
+ m_content = content;
+ return this;
+ }
+
+ public ServerAlarmRule setCreationDate(java.util.Date creationDate) {
+ m_createTime = creationDate;
+ return this;
+ }
+
+ public ServerAlarmRule setCreateTime(java.util.Date createTime) {
+ m_createTime = createTime;
+ return this;
+ }
+
+ public ServerAlarmRule setCreator(String creator) {
+ m_creator = creator;
+ return this;
+ }
+
+ public ServerAlarmRule setEndPoint(String endPoint) {
+ m_endPoint = endPoint;
+ return this;
+ }
+
+ public ServerAlarmRule setId(int id) {
+ m_id = id;
+ m_keyId = id;
+ return this;
+ }
+
+ public ServerAlarmRule setId(long id) {
+ m_id = id;
+ m_keyId = id;
+ return this;
+ }
+
+ public ServerAlarmRule setKeyId(int keyId) {
+ m_keyId = keyId;
+ return this;
+ }
+
+ public ServerAlarmRule setKeyId(long keyId) {
+ m_keyId = keyId;
+ return this;
+ }
+
+ public ServerAlarmRule setMeasurement(String measurement) {
+ m_measurement = measurement;
+ return this;
+ }
+
+ public ServerAlarmRule setTags(String tags) {
+ m_tags = tags;
+ return this;
+ }
+
+ public ServerAlarmRule setType(String type) {
+ m_type = type;
+ return this;
+ }
+
+ public ServerAlarmRule setUpdatetime(java.util.Date updatetime) {
+ m_updateTime = updatetime;
+ return this;
+ }
+
+ public ServerAlarmRule setUpdateTime(java.util.Date updateTime) {
+ m_updateTime = updateTime;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(1024);
+
+ sb.append("ServerAlarmRule[");
+ sb.append("category: ").append(m_category);
+ sb.append(", content: ").append(m_content);
+ sb.append(", create-time: ").append(m_createTime);
+ sb.append(", creator: ").append(m_creator);
+ sb.append(", end-point: ").append(m_endPoint);
+ sb.append(", id: ").append(m_id);
+ sb.append(", key-id: ").append(m_keyId);
+ sb.append(", measurement: ").append(m_measurement);
+ sb.append(", tags: ").append(m_tags);
+ sb.append(", type: ").append(m_type);
+ sb.append(", update-time: ").append(m_updateTime);
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/UserDefineRule.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/UserDefineRule.java
new file mode 100644
index 0000000000..70ed7ded6a
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/UserDefineRule.java
@@ -0,0 +1,115 @@
+package com.dianping.cat.alarm;
+
+
+public class UserDefineRule {
+ private long m_id;
+
+ private String m_content;
+
+ private java.util.Date m_createTime;
+
+ private java.util.Date m_updateTime;
+
+ private long m_keyId;
+
+ private long m_maxId;
+ public void afterLoad() {
+ m_keyId = m_id;
+ }
+
+ public String getContent() {
+ return m_content;
+ }
+
+ public java.util.Date getCreationDate() {
+ return m_createTime;
+ }
+
+ public java.util.Date getCreateTime() {
+ return m_createTime;
+ }
+
+ public long getId() {
+ return m_id;
+ }
+
+ public long getKeyId() {
+ return m_keyId;
+ }
+
+ public long getMaxId() {
+ return m_maxId;
+ }
+
+ public java.util.Date getUpdateTime() {
+ return m_updateTime;
+ }
+
+ public UserDefineRule setContent(String content) {
+ m_content = content;
+ return this;
+ }
+
+ public UserDefineRule setCreationDate(java.util.Date creationDate) {
+ m_createTime = creationDate;
+ return this;
+ }
+
+ public UserDefineRule setCreateTime(java.util.Date createTime) {
+ m_createTime = createTime;
+ return this;
+ }
+
+ public UserDefineRule setId(int id) {
+ m_id = id;
+ m_keyId = id;
+ return this;
+ }
+
+ public UserDefineRule setId(long id) {
+ m_id = id;
+ m_keyId = id;
+ return this;
+ }
+
+ public UserDefineRule setKeyId(int keyId) {
+ m_keyId = keyId;
+ return this;
+ }
+
+ public UserDefineRule setKeyId(long keyId) {
+ m_keyId = keyId;
+ return this;
+ }
+
+ public UserDefineRule setMaxId(int maxId) {
+ m_maxId = maxId;
+ return this;
+ }
+
+ public UserDefineRule setMaxId(long maxId) {
+ m_maxId = maxId;
+ return this;
+ }
+
+ public UserDefineRule setUpdateTime(java.util.Date updateTime) {
+ m_updateTime = updateTime;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(1024);
+
+ sb.append("UserDefineRule[");
+ sb.append("content: ").append(m_content);
+ sb.append(", create-time: ").append(m_createTime);
+ sb.append(", id: ").append(m_id);
+ sb.append(", key-id: ").append(m_keyId);
+ sb.append(", max-id: ").append(m_maxId);
+ sb.append(", update-time: ").append(m_updateTime);
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/BaseEntity.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/BaseEntity.java
new file mode 100644
index 0000000000..fcd9cd02b3
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/BaseEntity.java
@@ -0,0 +1,42 @@
+package com.dianping.cat.alarm.policy;
+
+import java.util.Formattable;
+import java.util.Formatter;
+
+import com.dianping.cat.alarm.policy.transform.DefaultXmlBuilder;
+
+public abstract class BaseEntity implements IEntity, Formattable {
+
+ public static final String XML = "%.3s";
+
+ public static final String XML_COMPACT = "%s";
+
+ protected void assertAttributeEquals(Object instance, String entityName, String name, Object expectedValue, Object actualValue) {
+ if (expectedValue == null && actualValue != null || expectedValue != null && !expectedValue.equals(actualValue)) {
+ throw new IllegalArgumentException(String.format("Mismatched entity(%s) found! Same %s attribute is expected! %s: %s.", entityName, name, entityName, instance));
+ }
+ }
+
+ protected boolean equals(Object o1, Object o2) {
+ if (o1 == null) {
+ return o2 == null;
+ } else if (o2 == null) {
+ return false;
+ } else {
+ return o1.equals(o2);
+ }
+ }
+
+ @Override
+ public void formatTo(Formatter formatter, int flags, int width, int precision) {
+ boolean compact = (precision == 0);
+ DefaultXmlBuilder builder = new DefaultXmlBuilder(compact);
+
+ formatter.format("%s", builder.buildXml(this));
+ }
+
+ @Override
+ public String toString() {
+ return new DefaultXmlBuilder().buildXml(this);
+ }
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/Constants.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/Constants.java
new file mode 100644
index 0000000000..2de9d8fb83
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/Constants.java
@@ -0,0 +1,26 @@
+package com.dianping.cat.alarm.policy;
+
+public class Constants {
+
+ public static final String ATTR_ID = "id";
+
+ public static final String ATTR_RECOVERMINUTE = "recoverMinute";
+
+ public static final String ATTR_SEND = "send";
+
+ public static final String ATTR_SUSPENDMINUTE = "suspendMinute";
+
+ public static final String ENTITY_ALERT_POLICY = "alert-policy";
+
+ public static final String ENTITY_GROUP = "group";
+
+ public static final String ENTITY_GROUPS = "groups";
+
+ public static final String ENTITY_LEVEL = "level";
+
+ public static final String ENTITY_LEVELS = "levels";
+
+ public static final String ENTITY_TYPE = "type";
+
+ public static final String ENTITY_TYPES = "types";
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/IEntity.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/IEntity.java
new file mode 100644
index 0000000000..1bddc46d2c
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/IEntity.java
@@ -0,0 +1,8 @@
+package com.dianping.cat.alarm.policy;
+
+public interface IEntity {
+ public void accept(IVisitor visitor);
+
+ public void mergeAttributes(T other);
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/IVisitor.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/IVisitor.java
new file mode 100644
index 0000000000..3c09a897d0
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/IVisitor.java
@@ -0,0 +1,17 @@
+package com.dianping.cat.alarm.policy;
+
+import com.dianping.cat.alarm.policy.entity.AlertPolicy;
+import com.dianping.cat.alarm.policy.entity.Group;
+import com.dianping.cat.alarm.policy.entity.Level;
+import com.dianping.cat.alarm.policy.entity.Type;
+
+public interface IVisitor {
+
+ public void visitAlertPolicy(AlertPolicy alertPolicy);
+
+ public void visitGroup(Group group);
+
+ public void visitLevel(Level level);
+
+ public void visitType(Type type);
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/AlertPolicy.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/AlertPolicy.java
new file mode 100644
index 0000000000..0812cf78c9
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/AlertPolicy.java
@@ -0,0 +1,83 @@
+package com.dianping.cat.alarm.policy.entity;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.dianping.cat.alarm.policy.BaseEntity;
+import com.dianping.cat.alarm.policy.IVisitor;
+
+public class AlertPolicy extends BaseEntity {
+ private Map m_types = new LinkedHashMap();
+
+ public AlertPolicy() {
+ }
+
+ @Override
+ public void accept(IVisitor visitor) {
+ visitor.visitAlertPolicy(this);
+ }
+
+ public AlertPolicy addType(Type type) {
+ m_types.put(type.getId(), type);
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof AlertPolicy) {
+ AlertPolicy _o = (AlertPolicy) obj;
+
+ if (!equals(getTypes(), _o.getTypes())) {
+ return false;
+ }
+
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public Type findType(String id) {
+ return m_types.get(id);
+ }
+
+ public Type findOrCreateType(String id) {
+ Type type = m_types.get(id);
+
+ if (type == null) {
+ synchronized (m_types) {
+ type = m_types.get(id);
+
+ if (type == null) {
+ type = new Type(id);
+ m_types.put(id, type);
+ }
+ }
+ }
+
+ return type;
+ }
+
+ public Map getTypes() {
+ return m_types;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+
+ hash = hash * 31 + (m_types == null ? 0 : m_types.hashCode());
+
+ return hash;
+ }
+
+ @Override
+ public void mergeAttributes(AlertPolicy other) {
+ }
+
+ public Type removeType(String id) {
+ return m_types.remove(id);
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Group.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Group.java
new file mode 100644
index 0000000000..cae86c01d6
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Group.java
@@ -0,0 +1,102 @@
+package com.dianping.cat.alarm.policy.entity;
+
+import static com.dianping.cat.alarm.policy.Constants.ATTR_ID;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_GROUP;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.dianping.cat.alarm.policy.BaseEntity;
+import com.dianping.cat.alarm.policy.IVisitor;
+
+public class Group extends BaseEntity {
+ private String m_id;
+
+ private Map m_levels = new LinkedHashMap();
+
+ public Group() {
+ }
+
+ public Group(String id) {
+ m_id = id;
+ }
+
+ @Override
+ public void accept(IVisitor visitor) {
+ visitor.visitGroup(this);
+ }
+
+ public Group addLevel(Level level) {
+ m_levels.put(level.getId(), level);
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Group) {
+ Group _o = (Group) obj;
+
+ if (!equals(getId(), _o.getId())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public Level findLevel(String id) {
+ return m_levels.get(id);
+ }
+
+ public Level findOrCreateLevel(String id) {
+ Level level = m_levels.get(id);
+
+ if (level == null) {
+ synchronized (m_levels) {
+ level = m_levels.get(id);
+
+ if (level == null) {
+ level = new Level(id);
+ m_levels.put(id, level);
+ }
+ }
+ }
+
+ return level;
+ }
+
+ public String getId() {
+ return m_id;
+ }
+
+ public Map getLevels() {
+ return m_levels;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+
+ hash = hash * 31 + (m_id == null ? 0 : m_id.hashCode());
+
+ return hash;
+ }
+
+ @Override
+ public void mergeAttributes(Group other) {
+ assertAttributeEquals(other, ENTITY_GROUP, ATTR_ID, m_id, other.getId());
+
+ }
+
+ public Level removeLevel(String id) {
+ return m_levels.remove(id);
+ }
+
+ public Group setId(String id) {
+ m_id = id;
+ return this;
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Level.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Level.java
new file mode 100644
index 0000000000..2aac2c6b7f
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Level.java
@@ -0,0 +1,107 @@
+package com.dianping.cat.alarm.policy.entity;
+
+import static com.dianping.cat.alarm.policy.Constants.ATTR_ID;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_LEVEL;
+
+import com.dianping.cat.alarm.policy.BaseEntity;
+import com.dianping.cat.alarm.policy.IVisitor;
+
+public class Level extends BaseEntity {
+ private String m_id;
+
+ private String m_send;
+
+ private Integer m_suspendMinute;
+
+ private Integer m_recoverMinute;
+
+ public Level() {
+ }
+
+ public Level(String id) {
+ m_id = id;
+ }
+
+ @Override
+ public void accept(IVisitor visitor) {
+ visitor.visitLevel(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Level) {
+ Level _o = (Level) obj;
+
+ if (!equals(getId(), _o.getId())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public String getId() {
+ return m_id;
+ }
+
+ public Integer getRecoverMinute() {
+ return m_recoverMinute;
+ }
+
+ public String getSend() {
+ return m_send;
+ }
+
+ public Integer getSuspendMinute() {
+ return m_suspendMinute;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+
+ hash = hash * 31 + (m_id == null ? 0 : m_id.hashCode());
+
+ return hash;
+ }
+
+ @Override
+ public void mergeAttributes(Level other) {
+ assertAttributeEquals(other, ENTITY_LEVEL, ATTR_ID, m_id, other.getId());
+
+ if (other.getSend() != null) {
+ m_send = other.getSend();
+ }
+
+ if (other.getSuspendMinute() != null) {
+ m_suspendMinute = other.getSuspendMinute();
+ }
+
+ if (other.getRecoverMinute() != null) {
+ m_recoverMinute = other.getRecoverMinute();
+ }
+ }
+
+ public Level setId(String id) {
+ m_id = id;
+ return this;
+ }
+
+ public Level setRecoverMinute(Integer recoverMinute) {
+ m_recoverMinute = recoverMinute;
+ return this;
+ }
+
+ public Level setSend(String send) {
+ m_send = send;
+ return this;
+ }
+
+ public Level setSuspendMinute(Integer suspendMinute) {
+ m_suspendMinute = suspendMinute;
+ return this;
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Type.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Type.java
new file mode 100644
index 0000000000..3eeab28f53
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/entity/Type.java
@@ -0,0 +1,102 @@
+package com.dianping.cat.alarm.policy.entity;
+
+import static com.dianping.cat.alarm.policy.Constants.ATTR_ID;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_TYPE;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.dianping.cat.alarm.policy.BaseEntity;
+import com.dianping.cat.alarm.policy.IVisitor;
+
+public class Type extends BaseEntity {
+ private String m_id;
+
+ private Map m_groups = new LinkedHashMap();
+
+ public Type() {
+ }
+
+ public Type(String id) {
+ m_id = id;
+ }
+
+ @Override
+ public void accept(IVisitor visitor) {
+ visitor.visitType(this);
+ }
+
+ public Type addGroup(Group group) {
+ m_groups.put(group.getId(), group);
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Type) {
+ Type _o = (Type) obj;
+
+ if (!equals(getId(), _o.getId())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public Group findGroup(String id) {
+ return m_groups.get(id);
+ }
+
+ public Group findOrCreateGroup(String id) {
+ Group group = m_groups.get(id);
+
+ if (group == null) {
+ synchronized (m_groups) {
+ group = m_groups.get(id);
+
+ if (group == null) {
+ group = new Group(id);
+ m_groups.put(id, group);
+ }
+ }
+ }
+
+ return group;
+ }
+
+ public Map getGroups() {
+ return m_groups;
+ }
+
+ public String getId() {
+ return m_id;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+
+ hash = hash * 31 + (m_id == null ? 0 : m_id.hashCode());
+
+ return hash;
+ }
+
+ @Override
+ public void mergeAttributes(Type other) {
+ assertAttributeEquals(other, ENTITY_TYPE, ATTR_ID, m_id, other.getId());
+
+ }
+
+ public Group removeGroup(String id) {
+ return m_groups.remove(id);
+ }
+
+ public Type setId(String id) {
+ m_id = id;
+ return this;
+ }
+
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultDomMaker.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultDomMaker.java
new file mode 100644
index 0000000000..d0fd90d155
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultDomMaker.java
@@ -0,0 +1,116 @@
+package com.dianping.cat.alarm.policy.transform;
+
+import static com.dianping.cat.alarm.policy.Constants.ATTR_ID;
+import static com.dianping.cat.alarm.policy.Constants.ATTR_RECOVERMINUTE;
+import static com.dianping.cat.alarm.policy.Constants.ATTR_SEND;
+import static com.dianping.cat.alarm.policy.Constants.ATTR_SUSPENDMINUTE;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.dianping.cat.alarm.policy.entity.AlertPolicy;
+import com.dianping.cat.alarm.policy.entity.Group;
+import com.dianping.cat.alarm.policy.entity.Level;
+import com.dianping.cat.alarm.policy.entity.Type;
+
+public class DefaultDomMaker implements IMaker {
+
+ @Override
+ public AlertPolicy buildAlertPolicy(Node node) {
+ AlertPolicy alertPolicy = new AlertPolicy();
+
+ return alertPolicy;
+ }
+
+ @Override
+ public Group buildGroup(Node node) {
+ String id = getAttribute(node, ATTR_ID);
+
+ Group group = new Group(id);
+
+ return group;
+ }
+
+ @Override
+ public Level buildLevel(Node node) {
+ String id = getAttribute(node, ATTR_ID);
+ String send = getAttribute(node, ATTR_SEND);
+ String suspendMinute = getAttribute(node, ATTR_SUSPENDMINUTE);
+ String recoverMinute = getAttribute(node, ATTR_RECOVERMINUTE);
+
+ Level level = new Level(id);
+
+ if (send != null) {
+ level.setSend(send);
+ }
+
+ if (suspendMinute != null) {
+ level.setSuspendMinute(convert(Integer.class, suspendMinute, null));
+ }
+
+ if (recoverMinute != null) {
+ level.setRecoverMinute(convert(Integer.class, recoverMinute, null));
+ }
+
+ return level;
+ }
+
+ @Override
+ public Type buildType(Node node) {
+ String id = getAttribute(node, ATTR_ID);
+
+ Type type = new Type(id);
+
+ return type;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T convert(Class type, String value, T defaultValue) {
+ if (value == null || value.length() == 0) {
+ return defaultValue;
+ }
+
+ if (type == Boolean.class || type == Boolean.TYPE) {
+ return (T) Boolean.valueOf(value);
+ } else if (type == Integer.class || type == Integer.TYPE) {
+ return (T) Integer.valueOf(value);
+ } else if (type == Long.class || type == Long.TYPE) {
+ return (T) Long.valueOf(value);
+ } else if (type == Short.class || type == Short.TYPE) {
+ return (T) Short.valueOf(value);
+ } else if (type == Float.class || type == Float.TYPE) {
+ return (T) Float.valueOf(value);
+ } else if (type == Double.class || type == Double.TYPE) {
+ return (T) Double.valueOf(value);
+ } else if (type == Byte.class || type == Byte.TYPE) {
+ return (T) Byte.valueOf(value);
+ } else if (type == Character.class || type == Character.TYPE) {
+ return (T) (Character) value.charAt(0);
+ } else {
+ return (T) value;
+ }
+ }
+
+ protected String getAttribute(Node node, String name) {
+ Node attribute = node.getAttributes().getNamedItem(name);
+
+ return attribute == null ? null : attribute.getNodeValue();
+ }
+
+ protected Node getChildTagNode(Node parent, String name) {
+ NodeList children = parent.getChildNodes();
+ int len = children.getLength();
+
+ for (int i = 0; i < len; i++) {
+ Node child = children.item(i);
+
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ if (child.getNodeName().equals(name)) {
+ return child;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultDomParser.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultDomParser.java
new file mode 100644
index 0000000000..0a38445621
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultDomParser.java
@@ -0,0 +1,149 @@
+package com.dianping.cat.alarm.policy.transform;
+
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_ALERT_POLICY;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_GROUP;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_LEVEL;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_TYPE;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import com.dianping.cat.alarm.policy.entity.AlertPolicy;
+import com.dianping.cat.alarm.policy.entity.Group;
+import com.dianping.cat.alarm.policy.entity.Level;
+import com.dianping.cat.alarm.policy.entity.Type;
+
+public class DefaultDomParser implements IParser {
+
+ protected Node getChildTagNode(Node parent, String name) {
+ NodeList children = parent.getChildNodes();
+ int len = children.getLength();
+
+ for (int i = 0; i < len; i++) {
+ Node child = children.item(i);
+
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ if (child.getNodeName().equals(name)) {
+ return child;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ protected List getChildTagNodes(Node parent, String name) {
+ NodeList children = parent.getChildNodes();
+ int len = children.getLength();
+ List nodes = new ArrayList(len);
+
+ for (int i = 0; i < len; i++) {
+ Node child = children.item(i);
+
+ if (child.getNodeType() == Node.ELEMENT_NODE) {
+ if (name == null || child.getNodeName().equals(name)) {
+ nodes.add(child);
+ }
+ }
+ }
+
+ return nodes;
+ }
+
+ protected Node getDocument(String xml) {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+
+ dbf.setIgnoringElementContentWhitespace(true);
+ dbf.setIgnoringComments(true);
+
+ try {
+ DocumentBuilder db = dbf.newDocumentBuilder();
+
+ return db.parse(new InputSource(new StringReader(xml)));
+ } catch (Exception x) {
+ throw new RuntimeException(x);
+ }
+ }
+
+ protected List getGrandChildTagNodes(Node parent, String name) {
+ Node child = getChildTagNode(parent, name);
+ NodeList children = child == null ? null : child.getChildNodes();
+ int len = children == null ? 0 : children.getLength();
+ List nodes = new ArrayList(len);
+
+ for (int i = 0; i < len; i++) {
+ Node grandChild = children.item(i);
+
+ if (grandChild.getNodeType() == Node.ELEMENT_NODE) {
+ nodes.add(grandChild);
+ }
+ }
+
+ return nodes;
+ }
+
+ public AlertPolicy parse(Node node) {
+ return parse(new DefaultDomMaker(), new DefaultLinker(false), node);
+ }
+
+ public AlertPolicy parse(String xml) throws SAXException, IOException {
+ Node doc = getDocument(xml);
+ Node rootNode = getChildTagNode(doc, ENTITY_ALERT_POLICY);
+
+ if (rootNode == null) {
+ throw new RuntimeException(String.format("alert-policy element(%s) is expected!", ENTITY_ALERT_POLICY));
+ }
+
+ return parse(new DefaultDomMaker(), new DefaultLinker(false), rootNode);
+ }
+
+ public AlertPolicy parse(IMaker maker, ILinker linker, Node node) {
+ AlertPolicy alertPolicy = maker.buildAlertPolicy(node);
+
+ if (node != null) {
+ AlertPolicy parent = alertPolicy;
+
+ for (Node child : getChildTagNodes(node, ENTITY_TYPE)) {
+ Type type = maker.buildType(child);
+
+ if (linker.onType(parent, type)) {
+ parseForType(maker, linker, type, child);
+ }
+ }
+ }
+
+ return alertPolicy;
+ }
+
+ public void parseForGroup(IMaker maker, ILinker linker, Group parent, Node node) {
+ for (Node child : getChildTagNodes(node, ENTITY_LEVEL)) {
+ Level level = maker.buildLevel(child);
+
+ if (linker.onLevel(parent, level)) {
+ parseForLevel(maker, linker, level, child);
+ }
+ }
+ }
+
+ public void parseForLevel(IMaker maker, ILinker linker, Level parent, Node node) {
+ }
+
+ public void parseForType(IMaker maker, ILinker linker, Type parent, Node node) {
+ for (Node child : getChildTagNodes(node, ENTITY_GROUP)) {
+ Group group = maker.buildGroup(child);
+
+ if (linker.onGroup(parent, group)) {
+ parseForGroup(maker, linker, group, child);
+ }
+ }
+ }
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultLinker.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultLinker.java
new file mode 100644
index 0000000000..2c2031ca07
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultLinker.java
@@ -0,0 +1,72 @@
+package com.dianping.cat.alarm.policy.transform;
+
+import java.util.ArrayList;
+import java.util.List;
+import com.dianping.cat.alarm.policy.entity.AlertPolicy;
+import com.dianping.cat.alarm.policy.entity.Group;
+import com.dianping.cat.alarm.policy.entity.Level;
+import com.dianping.cat.alarm.policy.entity.Type;
+
+public class DefaultLinker implements ILinker {
+ private boolean m_deferrable;
+
+ private List m_deferedJobs = new ArrayList();
+
+ public DefaultLinker(boolean deferrable) {
+ m_deferrable = deferrable;
+ }
+
+ public void finish() {
+ for (Runnable job : m_deferedJobs) {
+ job.run();
+ }
+ }
+
+ @Override
+ public boolean onGroup(final Type parent, final Group group) {
+ if (m_deferrable) {
+ m_deferedJobs.add(new Runnable() {
+ @Override
+ public void run() {
+ parent.addGroup(group);
+ }
+ });
+ } else {
+ parent.addGroup(group);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onLevel(final Group parent, final Level level) {
+ if (m_deferrable) {
+ m_deferedJobs.add(new Runnable() {
+ @Override
+ public void run() {
+ parent.addLevel(level);
+ }
+ });
+ } else {
+ parent.addLevel(level);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onType(final AlertPolicy parent, final Type type) {
+ if (m_deferrable) {
+ m_deferedJobs.add(new Runnable() {
+ @Override
+ public void run() {
+ parent.addType(type);
+ }
+ });
+ } else {
+ parent.addType(type);
+ }
+
+ return true;
+ }
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultSaxMaker.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultSaxMaker.java
new file mode 100644
index 0000000000..5404bc19c7
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultSaxMaker.java
@@ -0,0 +1,89 @@
+package com.dianping.cat.alarm.policy.transform;
+
+import static com.dianping.cat.alarm.policy.Constants.ATTR_ID;
+import static com.dianping.cat.alarm.policy.Constants.ATTR_RECOVERMINUTE;
+import static com.dianping.cat.alarm.policy.Constants.ATTR_SEND;
+import static com.dianping.cat.alarm.policy.Constants.ATTR_SUSPENDMINUTE;
+
+import org.xml.sax.Attributes;
+
+import com.dianping.cat.alarm.policy.entity.AlertPolicy;
+import com.dianping.cat.alarm.policy.entity.Group;
+import com.dianping.cat.alarm.policy.entity.Level;
+import com.dianping.cat.alarm.policy.entity.Type;
+
+public class DefaultSaxMaker implements IMaker {
+
+ @Override
+ public AlertPolicy buildAlertPolicy(Attributes attributes) {
+ AlertPolicy alertPolicy = new AlertPolicy();
+
+ return alertPolicy;
+ }
+
+ @Override
+ public Group buildGroup(Attributes attributes) {
+ String id = attributes.getValue(ATTR_ID);
+ Group group = new Group(id);
+
+ return group;
+ }
+
+ @Override
+ public Level buildLevel(Attributes attributes) {
+ String id = attributes.getValue(ATTR_ID);
+ String send = attributes.getValue(ATTR_SEND);
+ String suspendMinute = attributes.getValue(ATTR_SUSPENDMINUTE);
+ String recoverMinute = attributes.getValue(ATTR_RECOVERMINUTE);
+ Level level = new Level(id);
+
+ if (send != null) {
+ level.setSend(send);
+ }
+
+ if (suspendMinute != null) {
+ level.setSuspendMinute(convert(Integer.class, suspendMinute, null));
+ }
+
+ if (recoverMinute != null) {
+ level.setRecoverMinute(convert(Integer.class, recoverMinute, null));
+ }
+
+ return level;
+ }
+
+ @Override
+ public Type buildType(Attributes attributes) {
+ String id = attributes.getValue(ATTR_ID);
+ Type type = new Type(id);
+
+ return type;
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T convert(Class type, String value, T defaultValue) {
+ if (value == null || value.length() == 0) {
+ return defaultValue;
+ }
+
+ if (type == Boolean.class || type == Boolean.TYPE) {
+ return (T) Boolean.valueOf(value);
+ } else if (type == Integer.class || type == Integer.TYPE) {
+ return (T) Integer.valueOf(value);
+ } else if (type == Long.class || type == Long.TYPE) {
+ return (T) Long.valueOf(value);
+ } else if (type == Short.class || type == Short.TYPE) {
+ return (T) Short.valueOf(value);
+ } else if (type == Float.class || type == Float.TYPE) {
+ return (T) Float.valueOf(value);
+ } else if (type == Double.class || type == Double.TYPE) {
+ return (T) Double.valueOf(value);
+ } else if (type == Byte.class || type == Byte.TYPE) {
+ return (T) Byte.valueOf(value);
+ } else if (type == Character.class || type == Character.TYPE) {
+ return (T) (Character) value.charAt(0);
+ } else {
+ return (T) value;
+ }
+ }
+}
diff --git a/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultSaxParser.java b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultSaxParser.java
new file mode 100644
index 0000000000..3eba9a572b
--- /dev/null
+++ b/cat-alarm/src/main/java/com/dianping/cat/alarm/policy/transform/DefaultSaxParser.java
@@ -0,0 +1,273 @@
+package com.dianping.cat.alarm.policy.transform;
+
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_ALERT_POLICY;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_GROUP;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_LEVEL;
+import static com.dianping.cat.alarm.policy.Constants.ENTITY_TYPE;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.BufferedReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Stack;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import com.dianping.cat.alarm.policy.IEntity;
+import com.dianping.cat.alarm.policy.entity.AlertPolicy;
+import com.dianping.cat.alarm.policy.entity.Group;
+import com.dianping.cat.alarm.policy.entity.Level;
+import com.dianping.cat.alarm.policy.entity.Type;
+
+public class DefaultSaxParser extends DefaultHandler {
+
+ private DefaultLinker m_linker = new DefaultLinker(true);
+
+ private DefaultSaxMaker m_maker = new DefaultSaxMaker();
+
+ private Stack m_tags = new Stack();
+
+ private Stack