初学者应优先建立清晰责任边界、统一错误表达和可预测返回结构;业务逻辑与错误处理分离,用语义化业务异常类和controlleradvice分层响应;门面方法需意图明确、参数封装、职责单一。
初学者不必追求“高内聚、异常全拦截、洁净门面”这些术语堆砌出来的理想状态。真正关键的是:从第一行代码开始,就建立清晰的责任边界、统一的错误表达方式、以及可预测的返回结构。这比套用概念更重要。
把业务逻辑和错误处理分开
不要在 Service 方法里直接 try-catch 并打印日志或返回 null。错误不是被“吞掉”,而是被“识别”和“转化”。比如用户查询不存在,这不是程序异常,而是业务结果;而数据库连接失败,才是需要拦截的系统异常。
- 定义明确的业务异常类(如 UserNotFoundException),抛出时不带栈信息,只含语义化消息
- 用统一的 ControllerAdvice 拦截所有 RuntimeException,区分业务异常与系统异常,返回标准 JSON 结构(code/message/data)
- Service 层只专注“做事情”,不负责告诉前端怎么展示错误
门面(Facade)只暴露意图,不暴露实现
门面不是加一层空壳,而是提供一组符合业务场景的、有名字的方法。比如不要写 getXXXById,而写 loadUserProfile() 或 initiatePayment() —— 名字本身说明了用途和上下文。
- 门面方法参数尽量封装成 DTO,避免传一堆零散字段或 Map
- 每个门面方法只做一件事,且保证调用后状态可预期(成功/失败都明确)
- 不把 DAO、FeignClient、RedisTemplate 等细节透出门面,内部如何组合是实现细节
异常拦截要“分层响应”,不是“一锅端”
全拦截不等于全吞掉或全转成 500。不同层级的异常应有不同处理策略:
- Controller 层拦截并格式化输出(给前端看)
- Service 层抛出带上下文的异常(给开发看)
- 框架层(如 MyBatis、Redis)的原始异常,统一转为更友好的运行时异常,不暴露技术细节
从一个简单但完整的例子起步
比如写一个“创建订单”功能:
- 门面方法:createOrder(CreateOrderRequest request)
- 它调用库存校验、价格计算、支付初始化三个子服务
- 任一环节失败,都抛出对应业务异常(InsufficientStockException / InvalidPriceException)
- 全局异常处理器捕获后,统一返回 { "code": "ORDER_STOCK_SHORT", "message": "库存不足" }
这样既没耦合具体技术,又让错误可读、可定位、可测试。











