这篇文章以一个典型的电商后台为背景,完整走一遍"单体 → 模块化单体 → 提取微服务"的重构过程,重点展示每个阶段如何与 Claude Code 协作,包括真实会遇到的问题和应对方式。
项目背景是虚构的,但问题是真实的——这类遗留单体项目在国内企业里极为常见,重构思路和提示词可以直接参考。
背景:项目现状
项目名称: ShopMax 电商后台
技术栈: Spring Boot 2.6 + MySQL + Redis + RabbitMQ
代码规模: 15 万行,单一 Maven 项目
团队规模: 8 名后端开发
主要痛点:
- 所有代码在一个 Git 仓库,代码冲突严重
- 订单模块和商品模块无法独立部署
- 一个小 bug 需要重新部署整个系统(30分钟停机)
- 核心的
OrderService.java已达 4800 行
目标: 第一阶段先完成"模块化单体",第二阶段再拆成微服务
第一阶段:摸清家底
让 Claude Code 生成全景图
提示词:
请对这个 Spring Boot 项目做完整的架构分析。
项目位于当前目录。请:
1. 列出所有包名和各包的类数量
2. 找出所有 @Service 类,列出每个类的行数和公开方法数量
3. 分析模块间的调用关系(A Service 调用了哪些其他 Service)
4. 找出调用关系最复杂的前10个类(被最多其他类调用的)
5. 识别可能的"有界上下文"候选边界
Claude Code 输出(节选):
## 项目架构分析报告
### 包结构统计
com.shopmax.order - 23个类,47,230行
com.shopmax.product - 18个类,28,450行
com.shopmax.user - 12个类,15,680行
com.shopmax.payment - 9个类,12,100行
com.shopmax.inventory - 7个类,8,900行
com.shopmax.notification - 5个类,4,200行
com.shopmax.promotion - 11个类,19,800行
com.shopmax.common - 34个类,22,100行(工具类、基础设施)
### 最复杂的Service类(按行数)
1. OrderService.java - 4,823行,67个public方法 ⚠️
2. ProductService.java - 2,340行,34个public方法
3. PromotionService.java - 1,890行,28个public方法
4. UserService.java - 1,234行,22个public方法
5. InventoryService.java - 987行,18个public方法
### 调用关系图(被调用最多的类)
OrderService 被调用: 12个类
ProductService 被调用: 9个类
InventoryService 被调用: 7个类
UserService 被调用: 11个类
### ⚠️ 发现的问题
1. 循环依赖:OrderService ↔ PromotionService(相互调用)
2. 跨模块直接调用:order包中有6处直接调用inventory包的内部方法
3. com.shopmax.common 包职责混乱:包含了业务逻辑(不应该有)
### 建议的有界上下文边界
- 订单上下文(Order Context):order + 部分 inventory
- 商品目录上下文(Catalog Context):product + promotion
- 用户上下文(User Context):user
- 支付上下文(Payment Context):payment(已经相对独立)
- 库存上下文(Inventory Context):inventory(需从order中分离)
建立"绞杀者模式"基础
在把单体拆成微服务时,"绞杀者模式"(Strangler Fig Pattern)是最安全的策略:新旧系统并行,逐步把流量从旧系统迁移到新服务。
提示词:
我要使用绞杀者模式(Strangler Fig Pattern)来逐步拆分这个单体应用。
第一步是在单体内部建立清晰的模块边界(模块化单体),然后再把各模块提取为独立服务。
请帮我:
1. 在当前项目中创建 Maven 多模块结构的骨架(不移动任何现有代码)
目标结构:
shopmax-parent/
├── shopmax-order/
├── shopmax-product/
├── shopmax-user/
├── shopmax-payment/
├── shopmax-inventory/
└── shopmax-common/
2. 在父 pom.xml 中配置好依赖管理
3. 每个子模块只创建 pom.xml 和空的目录结构
4. 暂时不移动任何 Java 文件
完成后告诉我需要手动做哪些工作(比如IDE刷新)
第二阶段:整治 Common 包
common 包是最容易让人头疼的地方——历史上什么都往里塞,时间长了就变成了垃圾桶。
整理 Common 包
提示词:
请分析 com.shopmax.common 包下的所有类,
将它们分类到以下几组:
1. 纯工具类(无业务逻辑,无Spring注解)→ 保留在 common 模块
2. 通用基础设施(Redis工具、分布式锁、消息队列工具)→ 保留在 common 模块
3. 跨模块共用的数据结构(Result封装、分页类、通用异常)→ 保留在 common 模块
4. 混入了业务逻辑的类(比如 OrderUtils.java、ProductHelper.java)→ 需要移到对应业务模块
5. 应该是配置类但放错地方了 → 移到对应模块
生成一份分类报告,并标出需要移动的文件。
Claude Code 发现了一个棘手问题:
⚠️ 发现问题:OrderUtils.java 在 common 包中,但它依赖了 OrderRepository,
这违反了"工具类不应该有数据库依赖"的原则。
同时,这个类被 12 个不同的类引用。
建议处理方案:
将 OrderUtils 中的方法分两类处理:
- 纯计算方法(不依赖 Repository)→ 重命名为 OrderCalculator,移到 order 模块
- 依赖 Repository 的查询方法 → 合并到 OrderQueryService 中
这样移动会影响 12 个引用,需要批量修改 import。是否继续?
这正是 Claude Code 价值的体现:它能发现人工审查容易遗漏的隐性依赖。
第三阶段:啃最硬的骨头 —— 拆分 OrderService
4800 行的核心业务代码,这是整个重构里最不敢动、但又必须动的地方。
Step 1:深度分析 OrderService
提示词:
请对 OrderService.java 进行深度分析:
1. 识别所有的业务职责(不只是看方法名,要理解方法内容)
2. 绘制方法调用关系图(哪些方法调用哪些其他方法)
3. 标记出高风险方法(改动影响范围大、有复杂事务的)
4. 找出可以安全提取的"叶子方法"(不被其他方法调用,只调用Repository)
5. 估计重构时间(按你的判断)
Claude Code 的分析(节选):
## OrderService 深度分析报告
### 识别出的业务职责(7个,建议拆分为4-5个服务)
1. 订单创建流程(12个方法)- 下单、校验、库存锁定、价格计算
2. 订单查询(15个方法)- 各种查询条件、统计报表、导出
3. 订单状态流转(8个方法)- 支付确认、发货、完成、取消
4. 退款与售后(9个方法)- 申请退款、审核、退款执行
5. 促销计算(7个方法)- 优惠券、满减、折扣叠加计算(⚠️ 应该在 PromotionService)
6. 库存操作(6个方法)- 锁定库存、释放库存(⚠️ 应该在 InventoryService)
7. 通知发送(4个方法)- 下单通知、发货通知(⚠️ 应该在 NotificationService)
### ⚠️ 关键发现
OrderService 中有 17 个方法实际上属于其他模块的职责,
这些方法曾经被"临时"放在 OrderService 里,后来就没人移走了。
### 高风险方法(需要特别小心)
1. createOrder() - 调用了 7 个其他方法,3 个外部服务,有分布式事务
2. processRefund() - 调用支付网关,有补偿机制
3. calculateFinalPrice() - 促销计算逻辑极复杂,有17个分支
### 建议的拆分路径(从低风险到高风险)
阶段1:提取只读查询 → OrderQueryService(零风险)
阶段2:提取已在其他模块的方法 → 移回 InventoryService 等
阶段3:提取状态流转 → OrderStateService
阶段4:重构创建流程 → OrderCreationService(最复杂,最后做)
Step 2:执行低风险的第一步
提示词:
好,按照计划执行阶段1:提取 OrderQueryService。
以下方法提取到 OrderQueryService:
(粘贴 Claude Code 分析出的 15 个查询方法列表)
要求:
1. 新建 OrderQueryService.java
2. 所有方法加 @Transactional(readOnly = true)
3. 原 OrderService 中对应方法改为调用 orderQueryService.xxx()(保留作为门面)
4. 不要删除原 OrderService 中的方法,只是改成委托调用
执行前先告诉我你的完整计划,等我确认再开始。
Step 3:处理最棘手的循环依赖
在提取 OrderCreationService 时,发现了循环依赖问题:
OrderCreationService
→ PromotionService.calculateDiscount()
→ OrderService.getOrderHistoryCount() ← 这里形成循环!
提示词:
发现 OrderCreationService → PromotionService → OrderService 的循环依赖。
分析这个循环依赖的业务含义:
PromotionService.calculateDiscount() 需要知道用户的历史订单数量
(用于判断是否是新用户、VIP等级等),所以才调用了 OrderService。
请给出3种解决方案,分析各自的优缺点,
特别要考虑:
1. 方案是否会影响现有 API 的响应格式
2. 方案是否需要数据库表结构变更
3. 方案在高并发下是否安全
Claude Code 的回答:
## 循环依赖解决方案分析
### 方案1:数据冗余(推荐短期方案)
在 User 表中冗余存储 orderCount 字段,
PromotionService 直接从 User 表读取,不再调用 OrderService。
- 优点:改动最小,性能好(无需额外查询)
- 缺点:数据一致性风险(需要在每次创建/取消订单时更新该字段)
- 需要:数据库字段新增,OrderCreationService 中更新计数的逻辑
### 方案2:事件驱动(推荐长期方案)
PromotionService 维护自己的"用户促销资格"缓存,
通过监听 OrderCreatedEvent/OrderCancelledEvent 来更新缓存。
- 优点:彻底解耦,符合 DDD 原则
- 缺点:数据有最终一致性延迟(可接受)
- 需要:引入领域事件,增加 PromotionService 的内部状态
### 方案3:提取查询接口(保守方案)
定义 OrderQueryPort 接口,OrderService 实现它,
PromotionService 依赖接口而非具体类(依赖倒置)。
再将 getOrderHistoryCount 提取到专门的 UserOrderStatService。
- 优点:无需修改数据库,解耦清晰
- 缺点:增加了一个额外的服务类
### 我的推荐
短期:先用方案1(快,风险低,可以在2小时内完成)
长期:重构稳定后用方案2(在下个迭代做)
是否按照方案1继续?
这种"给出多方案 + 明确推荐"的响应模式,让团队可以做出有依据的架构决策。
第四阶段:数据层分离
业务代码拆开之后,更棘手的问题浮出来了:数据库表的归属问题。
数据所有权分析
提示词:
请分析项目的数据库表(通过 Entity 类推断),
生成一份"表所有权"分析:
1. 哪些表属于订单上下文(只有 order 相关代码读写)
2. 哪些表属于商品目录上下文
3. 哪些表被多个上下文共同读写(这些是数据分离的难点)
4. 对于共享的表,分析读写模式(谁是写入方,谁是读取方)
用表格格式输出。
发现了共享表问题:
⚠️ 发现问题:inventory 表被 3 个模块写入:
- OrderService 在创建订单时锁定库存(写)
- InventoryService 管理库存(写)
- ProductService 展示库存数量(读)
这是最典型的"共享可变状态"问题,是微服务拆分的主要障碍。
建议解决方案:
将库存操作统一归属 InventoryService,
其他模块通过接口调用(单体内直接调用,微服务化后改为HTTP/gRPC)。
OrderService 中的库存锁定方法应该委托给 InventoryService.lockStock()。
建立模块间 API 契约
提示词:
为了让单体内的模块间调用,将来能平滑迁移到微服务的HTTP调用,
请帮我为 InventoryService 定义一个"内部 API 接口":
1. 创建 InventoryPort.java 接口(放在 common 模块)
2. 在接口中定义:lockStock、releaseStock、deductStock、queryStock 方法
3. InventoryService 实现这个接口
4. 所有调用 InventoryService 的地方,改为注入 InventoryPort 接口
这样将来把 Inventory 拆成独立服务时,只需要换一个 HTTP 实现类,
调用方的代码不需要改变。
请同时为这个接口生成 OpenAPI 规范(用于将来的微服务 HTTP 接口文档)。
第五阶段:提取第一个微服务
模块边界清晰之后,终于到了真正"拆服务"的时候。支付模块依赖最少,是第一个开刀的对象。
准备独立部署
提示词:
payment 模块已经是相对独立的,计划将其提取为独立的 Spring Boot 微服务。
请帮我:
1. 基于现有的 payment 模块代码,创建一个新的独立 Spring Boot 项目结构
shopmax-payment-service/
├── src/main/java/com/shopmax/payment/
├── src/main/resources/application.yml
├── pom.xml
└── Dockerfile
2. 在新项目的 pom.xml 中,只包含 payment 模块需要的依赖
(不要把整个单体的依赖都带过去)
3. 修改 application.yml:
- 端口改为 8082
- 添加 Spring Actuator 健康检查
- 添加注册中心配置(Nacos,地址从环境变量读取)
4. 创建 Dockerfile(基于 openjdk:17-slim)
5. 在原单体中,将对 PaymentService 的直接调用改为通过 HTTP Client 调用
(使用 OpenFeign,接口定义放在 common 模块的 payment-api 包)
双写验证:零停机迁移
提示词:
在切换流量之前,我需要验证新的支付微服务和原单体中的支付模块行为一致。
请实现一个"双写比对"机制:
1. 在原单体中,PaymentServiceProxy 同时调用本地实现和远程微服务
2. 比对两个结果的差异,记录到日志表(payment_shadow_diff)
3. 差异率低于 0.1% 且持续24小时后,可以安全切换
4. 添加一个 Feature Flag:payment.use-remote-service=false/true
创建相关的日志表 DDL(Flyway migration)和对应的实体类。
复盘:这次重构学到了什么
重构完成后,ShopMax 的状态变成了:
- 一个模块化单体(6个边界清晰的模块,代码冲突从每天都有变成了几乎没有)
- 一个独立的支付微服务(可以单独发布,不再拖着整个系统重启)
- 模块间有明确的 API 接口契约(后续继续拆其他服务时路已经铺好了)
从这个案例里提炼几条结论:
Claude Code 最值钱的地方是"侦察",不是"执行"。分析阶段——识别依赖、扫描问题、生成报告——是它效率最高的地方。人工理清一个大型单体的依赖关系可能需要好几天,它几十分钟就能给出一张可读的结构图。这部分价值很容易被低估。
人和 AI 的分工要想清楚。让 AI 做执行性的事情效果好:迁移代码、批量重命名、生成模板、跑测试。但架构决策、方案取舍、业务逻辑判断,这些必须人来拍板,AI 给的只是选项,不是答案。
一次做太多是最常见的坑。当任务粒度太大时,Claude Code 容易遗漏内部依赖,编译失败后反而要花更多时间排查。更稳的节奏是:每次只做一件事,做完跑编译,没问题再继续。
CLAUDE.md 是值得认真维护的东西。把重构过程中的决策、规范、禁区都记下来,项目结束后这个文件本身就是一份有价值的架构决策记录(ADR),比很多专门写的文档更接地气。
测试不是可选项。没有测试就动核心模块,每次改完都是盲飞。先补测试再重构,这个顺序不应该被跳过。