2. Java核心工程化基础

当项目从“能跑”走向“可维护”,决定质量的不再是某个算法写得多巧,而是代码结构是否稳定、异常是否可控、日志是否可查、配置是否可换。本节围绕四个工程化支点展开:设计原则落地、包结构与分层、异常处理规范、日志与配置统一管理。它们共同决定一个 Java 工程能否被多人长期协作。

一、设计原则在工程中的落地方式

SOLID 原则不是口号,落到代码里通常表现为“依赖方向”和“变化隔离”。以订单创建为例:如果 OrderService 直接 new MySQLOrderRepository(),那么更换存储或增加缓存就必须改业务类,违反开闭原则与依赖倒置。工程中的常见做法是让业务层依赖接口,由外部注入实现:

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(long id);
}

public class OrderService {
    private final OrderRepository repository;
    private final PaymentGateway payment;

    public OrderService(OrderRepository repository, PaymentGateway payment) {
        this.repository = repository;
        this.payment = payment;
    }

    public Order create(CreateOrderCmd cmd) {
        Order order = Order.create(cmd);
        payment.charge(order);
        return repository.save(order);
    }
}

这里 OrderService 只依赖抽象,构造器注入让依赖关系显式化,也便于测试时替换为内存实现。单一职责则体现在:Order 负责业务不变量校验,OrderRepository 负责持久化,PaymentGateway 负责支付,三者变化原因不同,不应混在一个类里。接口隔离要求接口按调用方需要拆分,而不是做一个“大而全”的 OrderFacade。

二、包结构与分层架构

包结构是架构的第一层可见约束。推荐按“层 + 业务域”组织,而不是按类型堆放。例如:

com.example.order
├── api          // 对外接口与 DTO
├── application  // 用例编排、事务边界
├── domain       // 实体、值对象、领域服务、仓储接口
└── infrastructure // 仓储实现、外部网关、消息、配置

依赖方向必须单向:api → application → domain,infrastructure 实现 domain 定义的接口,由启动模块装配。禁止 domain 反向依赖 infrastructure,否则领域模型会被数据库、HTTP 框架污染。分层不是越多越好,小项目可合并 api 与 application,但“领域不依赖基础设施”这条线应尽量守住。

分层架构依赖方向与异常/日志贯穿关系 api 层:Controller / DTO / 参数校验 统一异常出口 · 请求日志 · 返回码映射 application 层:用例编排 / 事务边界 业务异常抛出 · 关键业务日志 · 配置读取 domain 层:实体 / 值对象 / 仓储接口 不依赖框架 · 不依赖日志实现 · 纯业务规则 infrastructure 层:仓储实现 / 外部网关 / 日志与配置适配 实现接口(依赖倒置)

三、异常处理规范

异常处理的核心是“分类 + 边界”。建议把异常分为三类:业务异常(可预期,如余额不足)、系统异常(不可预期,如数据库连接失败)、参数异常(校验失败)。业务异常应继承自定义基类,携带错误码,由 api 层统一转换为响应;系统异常记录完整堆栈并返回通用错误,避免泄露内部细节。

public class BizException extends RuntimeException {
    private final ErrorCode code;
    public BizException(ErrorCode code, String message) {
        super(message);
        this.code = code;
    }
    public ErrorCode getCode() { return code; }
}

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BizException.class)
    public ApiResponse<Void> handleBiz(BizException e) {
        log.warn("业务异常 code={} msg={}", e.getCode(), e.getMessage());
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleUnknown(Exception e) {
        log.error("系统异常", e);
        return ApiResponse.fail(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍后重试");
    }
}

规范要点:不要用 catch (Exception e) {} 吞掉异常;不要在循环里逐条打印堆栈;业务异常不打 error 级别,避免污染告警;跨层传递时保留原始异常作为 cause,便于定位根因。

四、日志框架集成与统一配置管理

日志应通过 SLF4J 门面编程,底层绑定 Logback 或 Log4j2,避免业务代码直接依赖具体实现。日志级别使用要克制:DEBUG 用于开发排查,INFO 记录关键状态变更,WARN 记录可恢复异常,ERROR 只留给需要人工介入的问题。推荐使用参数化占位符,避免字符串拼接开销:

log.info("订单创建成功 orderId={} userId={} amount={}", order.getId(), order.getUserId(), order.getAmount());

配置管理方面,应把可变参数外置到配置文件,并通过环境变量或配置中心覆盖。典型做法是 application.yml 提供默认值,敏感信息通过环境变量注入,不同环境用 profile 区分。配置项建议按模块前缀命名,例如 order.timeout-ms、order.retry.max,避免全局散落。启动时对关键配置做校验,缺失或非法值应快速失败,而不是运行到一半才报错。

工程化检查清单:依赖方向是否单向?异常是否有统一出口?日志是否使用门面与占位符?配置是否外置且可覆盖?这四点能在代码评审中快速判断一个模块是否“工程化达标”。

把设计原则、分层、异常、日志与配置放在一起看,它们其实在回答同一个问题:当需求变化、故障发生、环境切换时,系统是否还能被低成本地理解和修改。工程化基础不是额外负担,而是让后续并发、性能与稳定性优化有立足之地。