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