微服务DDD落地规范:内部抛异常、RPC外层Result封装

很多开发纠结:领域仓储 / 应用 Service 失败抛异常还是返回错误码 / Null/POJO ,RPC 接口能不能直接返回原始 POJO? 结合阿里 Java 开发手册、蚂蚁 DDD 落地规范、京东 / 字节落地实践、开源脚手架四大权威依据,拆解分层设计思想:进程内靠异常中断流程,跨进程 RPC 强制 Result 包装,禁止裸 POJO 返回

整体架构准则:对内异常、对外 Result;领域无错误码,接口无裸 POJO

一、分层规范定义

1. 应用内部(同 JVM:Repository→Domain→Application):优先抛出自定义业务异常,禁止返回 Null、错误码、Result

  1. Repository:查询聚合,不存在 / 数据非法直接抛领域异常,要么返回完整合法聚合根,要么抛异常,无中间态

  2. DomainService:业务规则校验失败(库存不足、价格变动)抛自定义异常;

  3. Application 应用层:异常向上透传,不做 if 判断捕获转错误返回。

示例(订单仓储标准代码)

java 复制代码
@Override
public Order findById(Long orderId) {
    Order order = orderMapper.selectById(orderId);
    // 不存在直接抛异常,不返回null
    if (order == null) {
        throw new OrderNotFoundException(ErrorCode.ORDER_NOT_FOUND.getCode(), "订单:" + orderId + "不存在");
    }
    List<OrderItem> itemList = orderItemMapper.selectList(
            Wrappers.lambdaQuery(OrderItem.class).eq(OrderItem::getOrderId, orderId)
    );
    order.setOrderItems(itemList == null ? new ArrayList<>() : itemList);
    return order;
}

2. 跨应用 RPC/HTTP 对外接口:统一 Result结构体封装,严禁直接返回原始 POJO

Dubbo、HTTP 接口出参固定格式:Result{Boolean success,String code,String msg,T data},消费端收到 Result 后,校验 success 失败,在本地转为 BizException 向上抛出,内部代码继续沿用异常编程风格。

java 复制代码
// RPC接口定义(规范)
Result<OrderDTO> getOrder(Long orderId);

// 消费端调用
Result<OrderDTO> result = orderRpc.getOrder(id);
if (!result.getSuccess()) {
    // 远端错误转为本地异常,内部继续异常编程
    throw new BizException(result.getCode(), result.getMsg());
}
OrderDTO dto = result.getData();

二、四大权威证据:证明「进程内抛异常是大厂统一标准」

证据 1:阿里《Java 开发手册》嵩山版官方强制规范

原文:应用内部推荐异常抛出;对外 HTTP、跨应用 RPC 调用统一使用 Result 结构体返回结果

  • 禁止在 Service、DAO 通过返回 null / 自定义 int 错误码标识业务失败;

  • 开放接口、跨进程 RPC 不能裸对象返回,必须统一结果包装。

出处:阿里云开发者社区公开 PDF 文档,全阿里产品线强制落地。

证据 2:蚂蚁集团 DDD 负责人殷浩官方落地文档

蚂蚁交易、支付、订单核心域落地规范:领域层(聚合、DomainService、Repository)禁止返回 Result / 错误码

  1. Result 属于表现层产物,领域层返回 Result 属于表现层污染领域模型;

  2. DDD 聚合完整性约束:聚合是一致性边界,加载结果只有两种:完整可用对象、业务异常,不存在残缺 / 空返回值;

出处:蚂蚁技术公众号《DDD 领域层落地最佳实践》。

证据 3:京东云开源 DDD 脚手架 + 京东中台编码规范

京东自营交易、订单中台分层规约:

  1. Domain 层校验失败抛出领域异常;

  2. Application 层透传异常,不做结果封装;

  3. 仅接口层全局异常捕获,统一组装 Result 返回。

出处:京东技术公众号、京东云开源项目源码。

证据 4:字节电商、抖音支付内部编码规约

字节内部编码规范:业务方法禁止使用返回码标识失败,异常作为进程内唯一失败出口;网关、RPC 框架统一拦截异常,转换成对外错误码结构体。

参考字节技术博客、内部基建开源组件。

三、为什么 RPC 不能直接返回原始 POJO而是Result?四大核心痛点

痛点 1:无法区分「业务空数据」和「调用异常」

裸 POJO 返回 null 存在二义性:

  • null = 订单业务上确实不存在;

  • null=RPC 超时、服务宕机、序列化异常、中间件故障。 调用方无法区分,极易引发线上空指针、业务逻辑错乱。 Result 依靠 success 字段天然区分:

  • success=true、data=null:业务无数据;

  • success=false:系统 / 业务异常,读取 code 定位错误。

痛点 2:跨语言 RPC 场景,Java 异常无法跨语言兼容

Java Exception 是语言特性,Go/PHP/Python 没有异常体系。服务端抛出自定义异常,跨语言调用时异常无法序列化解析,框架静默吞错,调用方只能拿到空对象。 Result 是通用 JSON / 二进制结构体,全编程语言兼容。

痛点 3:异常堆栈网络传输损耗高,大促压垮网络

异常携带全链路堆栈信息(类名、行号、调用栈),频繁报错场景下序列化 + 网络传输开销巨大。 Result 仅传输 code + 简短提示文案,异常栈保留在服务端本地落地日志,不经过网络传输,节省带宽与序列化 CPU 开销。

痛点 4:运维监控、熔断限流依赖统一错误码

Sentinel、监控大盘、链路追踪平台依靠 RPC 返回错误码做统计:业务报错率、异常告警、熔断降级。裸 POJO 无错误码字段,监控无法区分系统异常与正常空数据,治理能力失效。

四、Controller 层统一处理逻辑

通过@RestControllerAdvice全局异常处理器,统一捕获项目所有自定义异常,将异常内部携带的错误码、提示信息转为前端可见 Result。

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BizException.class)
    public Result<?> handleBizException(BizException e) {
        return Result.fail(e.getCode(), e.getMessage());
    }
}

微服务开发规范:只有 RPC 判断错误码,业务层永不检查返回值

**只有跨网络的 RPC / HTTP 接口需要判断错误码(Result)。 同进程内:查库、聚合、领域、业务方法 → 一律不检查返回值,失败直接抛异常!**

1. 同进程内(查库、Service、Domain、Repository)

规则:

**不检查返回值 不判断 null 不判断错误码 不返回 Result 失败直接抛异常**

示例:查订单库(永远这么写)

java 复制代码
public Order findById(Long orderId) {
    // 1. 查询
    Order order = orderMapper.selectById(orderId);

    // 2. 不存在直接抛异常
    if (order == null) {
        throw new OrderNotFoundException("订单不存在");
    }

    // 3. 正常返回完整对象
    return order;
}

业务层使用(不需要任何判断!

java 复制代码
// 正确!不用判断!不用检查!
Order order = orderRepository.findById(orderId);

2. 跨进程 / RPC / HTTP(唯一需要判断错误码)

规则:

**必须返回 Result 必须判断 success 失败后在本地重新抛异常**

示例:RPC 调用

java 复制代码
Result<OrderDTO> result = orderRpcService.getOrder(orderId);

// 只有这里需要判断!
if (!result.isSuccess()) {
    throw new BizException(result.getCode(), result.getMsg());
}

OrderDTO order = result.getData();

为什么 "只有 RPC 需要判断,其他都不用"?

1)同进程内:异常可以直接传递

  • 查库失败 → 抛异常

  • 业务异常 → 抛异常

  • 异常一路往上冒

  • 全局异常处理器统一捕获

不需要任何中间层判断!

2)跨进程 RPC:异常无法直接传递

  • 网络超时

  • 服务宕机

  • 序列化失败

  • 跨语言(Go/PHP 不识别 Java Exception)

必须用 Result 包装错误码

五、总结落地口诀

  1. 同进程内:不用错误码,失败抛异常,业务代码干净无泛滥 if 判断;

  2. 跨进程 RPC:不用裸 POJO,统一 Result 封装,屏蔽网络与异构语言问题;

  3. 前端接口:全局捕获异常,异常转对外错误码,内外分层互不污染。

拓展:什么场景才用返回错误码?

HTTP 接口出参、Dubbo RPC 出参两层,所有应用内部调用全采用异常机制。

相关推荐
C137的本贾尼1 小时前
MySQL 整体架构与存储引擎对比
数据库·mysql·架构
caimouse1 小时前
Reactos 第 4 章 对象管理 — 4.6 对象的访问控制 / 4.7 句柄的遗传和继承
开发语言·windows·架构
caimouse2 小时前
Reactos 第 4 章 对象管理 — 4.8 系统调用 NtDuplicateObject / 4.9 系统调用 NtClose
开发语言·windows·架构
贺国亚9 小时前
Multi-Agent与Multi-Task编排架构
架构
Qiuner12 小时前
Pico 重塑Agent时代人与数据交互方式
windows·docker·ai·架构
心之伊始15 小时前
MySQL EXPLAIN 执行计划实战:从 type、Extra 到慢 SQL 定位与优化
java·架构·源码分析·csdn
国科安芯15 小时前
国科安芯推出商业航天级抗辐照全双工 RS485/422 收发器 ASC491S2Y
网络·分布式·单片机·架构·安全性测试
一切皆是因缘际会15 小时前
AI智能新时代
数据结构·人工智能·ai·架构
微三云、小叶17 小时前
新型消费积分商业模式拆解:盈利架构、衰减铸造模型与项目风控要点
架构·软件开发·商业模式·本地生活·商业思维·私域运营