首页
学习
活动
专区
圈层
工具
发布

为什么Java里面,Service 层不直接返回 Result 对象?

Controller 一层很喜欢包Result<T>,到了 Service 这一层,我一般第一眼就不太想看见它。

不是说不能跑。是这玩意一旦进了 Service,后面八成会越写越别扭:业务逻辑和接口语义搅在一起,方法签名一眼看不出到底在干业务,还是在凑返回格式;今天给 Web 用还行,明天给 MQ 消费、定时任务、RPC 复用的时候,味儿就全变了。这个判断和你给的几篇参考文的气质是一致的:文章不是讲“规范”本身,而是盯着真实编码现场里最容易失控的地方下手。

先看一段很多项目里都见过的写法:

@Service

publicclass OrderService {

  public Result<OrderDTO> createOrder(CreateOrderCommand cmd) {

      if (cmd.getUserId() == null) {

          return Result.fail("用户不能为空");

      }

      Product product = productRepository.findById(cmd.getProductId());

      if (product == null) {

          return Result.fail("商品不存在");

      }

      if (product.getStock() <= 0) {

          return Result.fail("库存不足");

      }

      Order order = new Order(cmd.getUserId(), cmd.getProductId(), cmd.getCount());

      orderRepository.save(order);

      return Result.success(new OrderDTO(order.getId(), order.getStatus()));

  }

}

这段代码刚写出来,很多人觉得挺顺。校验、查库、保存、返回,一条龙。

但这个顺,只是 Controller 视角下的顺。

你把它放到 Service 层再看,就开始别扭了。

第一,Result.success()、Result.fail()这种东西,本质是接口返回协议。是给前端、给调用方、给 HTTP 用的。它应该出现在边界,不应该混进核心业务里。Service 的职责是“下单成功”还是“库存不足”,不是“code=200 还是 code=5001”。

第二,Service 一旦返回Result,调用方就会被迫跟着它的节奏走。

比如你后面有个定时补单任务,也要调这个方法:

@Scheduled(cron = "0/10 * * * * ?")

public void retryCreateOrder() {

  CreateOrderCommand cmd = loadRetryTask();

  Result<OrderDTO> result = orderService.createOrder(cmd);

  if (!result.isSuccess()) {

      log.warn("补单失败: {}", result.getMessage());

      return;

  }

  log.info("补单成功: {}", result.getData().getId());

}

问题来了。定时任务根本不关心你是不是Result,它只关心成功没成功,失败是什么原因,要不要重试。

这时候Result就成了一个夹生层。看着像统一了,实际上是把 Web 层的壳子硬塞给所有调用方。

更麻烦的是,很多项目最后会演变成这样:

public Result<OrderDTO> createOrder(CreateOrderCommand cmd) {

  try {

      // 一堆业务逻辑

  } catch (Exception e) {

      log.error("下单异常", e);

      return Result.fail("系统异常");

  }

}

我对这种写法一直比较嫌弃。

因为异常被你在 Service 吃掉以后,事务回滚、重试、告警、错误分类,都会变得很钝。你在 Controller 里看见的只是一个Result.fail("系统异常"),但真实现场早就没了。

Service 层更合适的做法,是返回明确的业务对象,或者干脆返回void,失败就抛业务异常。

比如这样:

@Service

publicclass OrderService {

  @Transactional(rollbackFor = Exception.class)

  public Order createOrder(CreateOrderCommand cmd) {

      if (cmd.getUserId() == null) {

          thrownew BizException("用户不能为空");

      }

      Product product = productRepository.findById(cmd.getProductId());

      if (product == null) {

          thrownew BizException("商品不存在");

      }

      if (product.getStock() < cmd.getCount()) {

          thrownew BizException("库存不足");

      }

      product.decreaseStock(cmd.getCount());

      Order order = new Order(cmd.getUserId(), cmd.getProductId(), cmd.getCount());

      orderRepository.save(order);

      return order;

  }

}

然后让 Controller 去做它该做的包装:

@RestController

@RequestMapping("/orders")

publicclass OrderController {

  @PostMapping

  public Result<Long> create(@RequestBody CreateOrderRequest request) {

      CreateOrderCommand cmd = new CreateOrderCommand(

              request.getUserId(),

              request.getProductId(),

              request.getCount()

      );

      Order order = orderService.createOrder(cmd);

      return Result.success(order.getId());

  }

}

这样拆开以后,层次就干净了。

Service 只表达业务:能不能下单,为什么不能下单,下单后产出什么。

Controller 只表达协议:HTTP 返回什么结构,错误码怎么映射,提示文案怎么出。

别小看这层分离,后面复用的时候差别很大。

比如 MQ 消费下单:

@RabbitListener(queues = "order.create.queue")

public void consume(OrderCreateMessage message) {

  CreateOrderCommand cmd = new CreateOrderCommand(

          message.getUserId(),

          message.getProductId(),

          message.getCount()

  );

  orderService.createOrder(cmd);

}

这里就很自然。消费成功就是成功,失败直接抛异常,让 MQ 走重试或死信。你要是返回Result,还得再判断一遍isSuccess(),本来该靠异常机制解决的事,硬是绕了一层字符串判断。

还有一个经常被忽略的问题:Result会污染领域建模。

有些人写着写着,Repository、Manager、Feign 适配层全开始返回Result。最后每一层都在if (!result.isSuccess()) return result;。代码看起来统一,实际上业务链路全是胶水。

真正稳定的代码不是“每层都长一样”,而是“每层只干自己的事”。

当然,也不是说 Service 层绝对不能返回包装对象。

有一种情况可以接受:这个返回对象本身就是业务结果,而不是接口结果。比如支付处理后的状态聚合:

public class PayResult {

  private boolean paid;

  private String channelOrderNo;

  private String failReason;

}

这没问题。因为它表达的是业务结果,不是 HTTP 协议里的code/msg/data三件套。

所以这事最后落下来,其实就一句话:

Result属于接口边界,不属于业务核心。

Controller 返回Result,很正常。 Service 返回领域对象、业务结果、或者直接抛异常,这才顺手。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OlhHSfjIfLF65MJCzW7BeQaQ0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券