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

一、分层规范定义
1. 应用内部(同 JVM:Repository→Domain→Application):优先抛出自定义业务异常,禁止返回 Null、错误码、Result
-
Repository:查询聚合,不存在 / 数据非法直接抛领域异常,要么返回完整合法聚合根,要么抛异常,无中间态;
-
DomainService:业务规则校验失败(库存不足、价格变动)抛自定义异常;
-
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 / 错误码。
-
Result 属于表现层产物,领域层返回 Result 属于表现层污染领域模型;
-
DDD 聚合完整性约束:聚合是一致性边界,加载结果只有两种:完整可用对象、业务异常,不存在残缺 / 空返回值;
出处:蚂蚁技术公众号《DDD 领域层落地最佳实践》。
证据 3:京东云开源 DDD 脚手架 + 京东中台编码规范
京东自营交易、订单中台分层规约:
-
Domain 层校验失败抛出领域异常;
-
Application 层透传异常,不做结果封装;
-
仅接口层全局异常捕获,统一组装 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 包装错误码。
五、总结落地口诀
-
同进程内:不用错误码,失败抛异常,业务代码干净无泛滥 if 判断;
-
跨进程 RPC:不用裸 POJO,统一 Result 封装,屏蔽网络与异构语言问题;
-
前端接口:全局捕获异常,异常转对外错误码,内外分层互不污染。
拓展:什么场景才用返回错误码?
仅HTTP 接口出参、Dubbo RPC 出参两层,所有应用内部调用全采用异常机制。