Java 架构中的“演进式重构”:如何在不停机的前提下持续变好
Java 架构中的“演进式重构”:如何在不停机的前提下持续变好
很多 Java 系统并不是一开始就“架构不合理”,而是在业务持续增长的过程中慢慢变形的。最真实的状态往往是:系统还能跑、需求还能接,但每次改动都心里发虚;新人不敢动老代码;线上问题越来越依赖“记忆”和“经验”。
这篇文章不讲“推倒重来”,而是讲一种更现实的方式:演进式重构(Evolutionary Architecture)。目标只有一个——在不停机、不大改的前提下,让系统一年比一年好维护。
1)先判断问题层级:不是所有问题都该“重构”
在开始之前,一定要分清三类问题:
- 代码层问题:方法太长、命名混乱、重复逻辑多
- 模块层问题:职责不清、互相调用混乱、改一处影响多处
- 架构层问题:事务边界模糊、强依赖外部系统、同步链路过长
很多团队一上来就喊“要重构架构”,结果半年过去,业务没跟上,架构也没真正落地。正确顺序通常是:
> 先稳住架构层 → 再整理模块层 → 最后慢慢清理代码层
否则你会陷入“永远在重构,但问题还在”的循环。
2)第一步:给系统“止血”——稳定事务与边界
最常见的隐患是: 一个 HTTP 请求里,串了数据库 + 缓存 + MQ + 第三方接口,任何一步慢,整体就慢;任何一步失败,事务就难以收场。
2.1 明确一个原则:事务只包“本地一致性”
@Transactional
public void createOrder(CreateOrderCmd cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
// ❌ 错误示例:事务中直接调外部系统
paymentClient.prePay(order.getId());
}
演进式改法不是“一次全拆”,而是先把外部依赖挪出事务。
@Transactional
public Long createOrder(CreateOrderCmd cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
outboxRepository.save(
OutboxEvent.of("ORDER_CREATED", order.getId())
);
return order.getId();
}
> 第一步先保证:事务只做“写库”这件事 > 后续流程,交给异步或事件处理
这是“止血级”的改动,风险小,收益大。
3)第二步:引入“用例层”,切断 Controller → Service 乱流
很多系统的问题不是“功能多”,而是Controller 直接调各种 Service,Service 之间互相调用,形成网状结构。
演进式解法: 加一层 Application / UseCase,不删旧 Service,只“收口”新入口。
@RestController
public class OrderController {
private final PlaceOrderUseCase placeOrderUseCase;
@PostMapping("/orders")
public Long place(@RequestBody PlaceOrderCmd cmd) {
return placeOrderUseCase.execute(cmd);
}
}
@Service
public class PlaceOrderUseCase {
private final OrderService orderService;
private final CouponService couponService;
public Long execute(PlaceOrderCmd cmd) {
couponService.lock(cmd.getCouponId());
return orderService.create(cmd);
}
}
好处是:
- Controller 不再关心业务流程
- Service 不再互相“随意调用”
- 新需求优先走 UseCase,旧代码慢慢收敛
这一步不需要重写旧代码,但会逐渐改变系统结构走向。
4)第三步:让 Domain “有权力”,减少 Service if-else
典型老系统问题: 业务规则散落在各个 Service 的 if-else 里,状态判断不统一。
演进式方式不是“上 DDD 全套”,而是先把最容易出错的规则收进去。
public class Order {
private OrderStatus status;
public void pay() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("order cannot be paid");
}
this.status = OrderStatus.PAID;
}
public void cancel() {
if (status == OrderStatus.PAID) {
throw new IllegalStateException("paid order cannot cancel");
}
this.status = OrderStatus.CANCELLED;
}
}
Service 里只负责“调用”,不负责“判断”。
public void handlePay(Long orderId) {
Order order = orderRepo.find(orderId);
order.pay();
orderRepo.save(order);
}
你会发现一个变化: bug 更容易集中暴露在 Domain,而不是散落在各处。
5)第四步:为“不可控因素”预留缓冲区
当系统开始变复杂,不确定性一定会上升。演进式重构必须提前处理:
- 重复请求
- 重试
- 回调
- 超时
5.1 幂等是“低成本高回报”的改造
public void handlePaymentCallback(String callbackId, Long orderId) {
if (callbackRepo.exists(callbackId)) {
return;
}
Order order = orderRepo.find(orderId);
order.pay();
orderRepo.save(order);
callbackRepo.save(callbackId);
}
甚至可以更激进一点: 用数据库唯一索引直接兜底幂等,代码反而更简单。
6)第五步:让系统“看得见”自己的状态
很多团队其实不是不会修 bug,而是不知道问题在哪。
演进式重构里,非常关键的一步是: 在不改业务的前提下,补可观测性。
6.1 统一 TraceId
public class TraceUtil {
public static String init() {
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
return traceId;
}
public static void clear() {
MDC.remove("traceId");
}
}
日志里统一带:
[%X{traceId}] orderId=xxx cost=xxms
当系统出问题时,你不再靠“猜”,而是靠“串”。
7)什么时候才该考虑“拆服务”?
这是很多人最关心的,但也是最后才该问的问题。
一个经验判断标准:
- 如果模块边界还不清晰 → 不要拆
- 如果事务和一致性没想清楚 → 不要拆
- 如果只是“觉得微服务更高级” → 千万别拆
当你已经做到:
- 用例层清晰
- Domain 承载规则
- 外部依赖已解耦
- 异步与幂等成熟
这时拆服务,成本会低很多,而且不会“拆完更乱”。
结语:好的 Java 架构,是“每天都能改一点”
演进式重构的核心思想只有一句话:
> 不要等“有时间再重构”,而是让每一次改动都朝着更清晰的结构前进。
你不需要一次性引入所有“先进理念”, 只要做到这几点,系统就会慢慢变好:
- 事务只做该做的事
- 流程集中、入口收口
- 规则进 Domain
- 不确定性用机制兜住
- 问题能被快速定位
架构不是一次性工程,而是一种长期习惯。 当这种习惯形成,系统的“可维护性”会自然显现出来。
评论 0