第四篇:完整案例演示 —— 用 Claude Code 驱动单体到微服务的重构

11 阅读13分钟

这篇文章以一个典型的电商后台为背景,完整走一遍"单体 → 模块化单体 → 提取微服务"的重构过程,重点展示每个阶段如何与 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),比很多专门写的文档更接地气。

测试不是可选项。没有测试就动核心模块,每次改完都是盲飞。先补测试再重构,这个顺序不应该被跳过。