Spring Boot 后端接口分层设计:从 Controller 到统一异常处理 📅 2026/7/6 2:35:40 在 Java 后端项目里接口能不能长期维护很多时候不取决于某个框架特性而取决于最基础的工程结构请求在哪里校验、业务逻辑放在哪里、异常怎么返回、对象如何在层与层之间流转。这篇文章以 Spring Boot 为例整理一套适合中小型业务系统的接口分层实践。它不追求复杂架构而是解决日常开发中最常见的几个问题Controller 越写越厚业务逻辑散落在接口层。Entity 直接暴露给前端字段变化容易牵连接口。异常返回格式不统一前端需要兼容多种错误结构。Service 层缺少边界事务、校验、组装逻辑混在一起。一、推荐的后端分层结构一个常见的 Spring Boot 模块可以按下面方式组织src/main/java/com/example/order ├── controller ├── service │ └── impl ├── repository ├── domain ├── dto │ ├── request │ └── response ├── converter ├── exception └── common各层职责可以这样划分层级主要职责Controller接收 HTTP 请求、参数校验、调用 Service、返回响应Service编排业务流程、控制事务边界、处理业务规则Repository数据访问封装数据库操作Domain / Entity领域对象或数据库实体DTO接口入参和出参对象Converter负责 DTO、Entity、VO 之间的转换Exception自定义异常和统一异常处理核心原则是Controller 只做接口适配Service 承担业务表达Repository 只关心数据访问。二、不要让 Controller 承担业务逻辑很多项目后期难维护是从 Controller 变厚开始的。比如下面这种写法PostMapping(/orders) public ApiResponseLong createOrder(RequestBody CreateOrderRequest request) { User user userRepository.findById(request.getUserId()) .orElseThrow(() - new RuntimeException(用户不存在)); if (request.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new RuntimeException(订单金额必须大于 0); } Order order new Order(); order.setUserId(user.getId()); order.setAmount(request.getAmount()); order.setStatus(OrderStatus.CREATED); orderRepository.save(order); return ApiResponse.success(order.getId()); }这段代码短期看很直接但它把用户查询、金额校验、订单创建、状态初始化、数据库保存都放进了接口层。后续如果有小程序、后台管理系统、批处理任务都需要创建订单就很容易复制逻辑。更好的方式是让 Controller 保持薄一点PostMapping(/orders) public ApiResponseLong createOrder(Valid RequestBody CreateOrderRequest request) { Long orderId orderService.createOrder(request); return ApiResponse.success(orderId); }业务逻辑移动到 ServiceService public class OrderServiceImpl implements OrderService { private final UserRepository userRepository; private final OrderRepository orderRepository; public OrderServiceImpl(UserRepository userRepository, OrderRepository orderRepository) { this.userRepository userRepository; this.orderRepository orderRepository; } Override Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderRequest request) { User user userRepository.findById(request.getUserId()) .orElseThrow(() - new BusinessException(USER_NOT_FOUND, 用户不存在)); if (request.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(INVALID_ORDER_AMOUNT, 订单金额必须大于 0); } Order order new Order(); order.setUserId(user.getId()); order.setAmount(request.getAmount()); order.setStatus(OrderStatus.CREATED); orderRepository.save(order); return order.getId(); } }这样做之后接口层只负责 HTTP 语义业务层负责业务语义。后续即使换成消息消费、定时任务或内部 RPC 调用也能复用 Service 逻辑。三、接口入参与出参使用 DTO很多新项目为了省事会直接把 Entity 暴露给前端GetMapping(/orders/{id}) public Order getOrder(PathVariable Long id) { return orderRepository.findById(id).orElseThrow(); }这会带来几个问题数据库字段变化会影响接口响应。敏感字段可能意外返回给前端。接口结构被数据库模型绑死不利于演进。前端需要的展示字段通常不是单个 Entity 能表达的。建议使用独立的 Response DTOpublic class OrderDetailResponse { private Long id; private String orderNo; private BigDecimal amount; private String status; private String statusText; private LocalDateTime createdAt; // getter/setter }Controller 返回 DTOGetMapping(/orders/{id}) public ApiResponseOrderDetailResponse getOrder(PathVariable Long id) { return ApiResponse.success(orderService.getOrderDetail(id)); }转换逻辑可以放在 Converter 中public class OrderConverter { private OrderConverter() { } public static OrderDetailResponse toDetailResponse(Order order) { OrderDetailResponse response new OrderDetailResponse(); response.setId(order.getId()); response.setOrderNo(order.getOrderNo()); response.setAmount(order.getAmount()); response.setStatus(order.getStatus().name()); response.setStatusText(order.getStatus().getDescription()); response.setCreatedAt(order.getCreatedAt()); return response; } }DTO 的价值在于隔离变化。Entity 面向数据库DTO 面向接口契约二者不应该强绑定。四、使用 Bean Validation 做基础参数校验基础字段校验可以交给 Bean Validation不要在业务代码里堆大量空值判断。public class CreateOrderRequest { NotNull(message 用户 ID 不能为空) private Long userId; NotNull(message 订单金额不能为空) DecimalMin(value 0.01, message 订单金额必须大于 0) private BigDecimal amount; NotEmpty(message 商品列表不能为空) private ListOrderItemRequest items; // getter/setter }Controller 中增加ValidPostMapping(/orders) public ApiResponseLong createOrder(Valid RequestBody CreateOrderRequest request) { return ApiResponse.success(orderService.createOrder(request)); }需要注意的是Bean Validation 适合做格式类、必填类、范围类校验。涉及数据库查询、库存判断、状态流转这类业务规则仍然应该放在 Service 层。五、统一响应结构统一响应结构能减少前后端协作成本。一个简单的返回结构可以这样设计public class ApiResponseT { private boolean success; private String code; private String message; private T data; public static T ApiResponseT success(T data) { ApiResponseT response new ApiResponse(); response.success true; response.code OK; response.message success; response.data data; return response; } public static T ApiResponseT failure(String code, String message) { ApiResponseT response new ApiResponse(); response.success false; response.code code; response.message message; return response; } // getter/setter }成功响应{ success: true, code: OK, message: success, data: { id: 1001, orderNo: PO202607050001 } }失败响应{ success: false, code: USER_NOT_FOUND, message: 用户不存在, data: null }响应结构不需要一开始设计得很复杂但最好从项目早期就统一下来。六、自定义业务异常不要在业务代码里直接抛RuntimeException因为它缺少稳定的错误码也不利于前端处理。可以定义一个业务异常public class BusinessException extends RuntimeException { private final String code; public BusinessException(String code, String message) { super(message); this.code code; } public String getCode() { return code; } }在 Service 中使用if (!order.canCancel()) { throw new BusinessException(ORDER_CANNOT_CANCEL, 当前订单状态不允许取消); }错误码建议稳定、可枚举、可搜索。错误提示可以调整但错误码一旦被前端、客户端或第三方系统依赖就不要轻易改变。七、统一异常处理Spring Boot 可以通过RestControllerAdvice统一处理接口异常RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResponseEntityApiResponseVoid handleBusinessException(BusinessException ex) { return ResponseEntity .badRequest() .body(ApiResponse.failure(ex.getCode(), ex.getMessage())); } ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntityApiResponseVoid handleValidationException(MethodArgumentNotValidException ex) { String message ex.getBindingResult() .getFieldErrors() .stream() .findFirst() .map(FieldError::getDefaultMessage) .orElse(请求参数不合法); return ResponseEntity .badRequest() .body(ApiResponse.failure(INVALID_PARAM, message)); } ExceptionHandler(Exception.class) public ResponseEntityApiResponseVoid handleException(Exception ex) { return ResponseEntity .internalServerError() .body(ApiResponse.failure(INTERNAL_SERVER_ERROR, 系统繁忙请稍后重试)); } }这里有一个重要细节返回给前端的错误信息要克制日志里记录完整异常接口响应只返回用户能理解、也适合暴露的信息。实际项目中可以在兜底异常里加日志private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class); ExceptionHandler(Exception.class) public ResponseEntityApiResponseVoid handleException(Exception ex) { log.error(Unexpected server error, ex); return ResponseEntity .internalServerError() .body(ApiResponse.failure(INTERNAL_SERVER_ERROR, 系统繁忙请稍后重试)); }八、事务边界放在 Service 层事务注解一般放在 Service 的 public 方法上Override Transactional(rollbackFor Exception.class) public void payOrder(Long orderId) { Order order orderRepository.findById(orderId) .orElseThrow(() - new BusinessException(ORDER_NOT_FOUND, 订单不存在)); order.pay(); orderRepository.save(order); paymentRecordRepository.save(PaymentRecord.of(order)); }不建议把事务放在 Controller 层因为 Controller 是接口适配层不应该关心数据库事务。不建议把事务过度下沉到每个 Repository 方法因为一次业务操作往往包含多个数据库动作需要统一提交或回滚。事务方法里还要注意几点避免在事务中做耗时外部调用例如 HTTP 请求、文件上传、大批量远程查询。注意 Spring AOP 的自调用问题同一个类内部方法互相调用可能导致事务不生效。对只读查询可以使用Transactional(readOnly true)。九、一个完整接口的最终形态ControllerRestController RequestMapping(/api/orders) public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } PostMapping public ApiResponseLong createOrder(Valid RequestBody CreateOrderRequest request) { return ApiResponse.success(orderService.createOrder(request)); } GetMapping(/{id}) public ApiResponseOrderDetailResponse getOrder(PathVariable Long id) { return ApiResponse.success(orderService.getOrderDetail(id)); } }Servicepublic interface OrderService { Long createOrder(CreateOrderRequest request); OrderDetailResponse getOrderDetail(Long id); }Service 实现Service public class OrderServiceImpl implements OrderService { private final OrderRepository orderRepository; public OrderServiceImpl(OrderRepository orderRepository) { this.orderRepository orderRepository; } Override Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderRequest request) { Order order Order.create(request.getUserId(), request.getAmount()); orderRepository.save(order); return order.getId(); } Override Transactional(readOnly true) public OrderDetailResponse getOrderDetail(Long id) { Order order orderRepository.findById(id) .orElseThrow(() - new BusinessException(ORDER_NOT_FOUND, 订单不存在)); return OrderConverter.toDetailResponse(order); } }这套结构并不复杂但它把接口、业务、数据、异常、响应分别放在了合适的位置。十、总结Spring Boot 项目的可维护性往往来自一些朴素但稳定的习惯Controller 保持轻薄只处理 HTTP 层逻辑。Service 承载业务流程和事务边界。Repository 专注数据访问。Entity 不直接暴露给前端接口使用 DTO。参数校验使用 Bean Validation业务校验放在 Service。统一响应结构统一异常处理。错误码稳定错误信息清晰异常日志完整。这些实践不会让项目一夜之间变成复杂架构但能让代码在需求增长时仍然保持清晰。对于多数 Java 后端系统来说先把这些基本功做好比过早引入复杂设计更重要。