Java与Spring Boot分层架构下的异常处理最佳实践咨询
嘿,刚上手Java和Spring Boot就关注分层架构里的异常处理最佳实践,这点真的值得点赞!我结合实际项目踩过的坑和经验,给你拆解下这个问题~
一、各分层该在哪抛出异常?
核心原则是让异常和分层的职责匹配,不同层抛的异常要对应各自的关注点:
1. Repository层
Repository层的核心职责是封装数据库CRUD操作,所以这里尽量少手动抛异常:
- 数据库层面的异常(比如SQL语法错误、唯一键约束违反、查询无结果),Spring Data JPA/MyBatis这类框架会自动抛出对应的RuntimeException(比如
EmptyResultDataAccessException、DataIntegrityViolationException),这些异常可以直接往上抛给Service层,不用自己捕获再包装。 - 除非你在Repository里做了极少量的数据合法性校验(比如查询到的状态不符合预期),这时候可以抛出通用的
IllegalStateException,但这种情况建议尽量把校验逻辑移到Service层,保持Repository的纯粹性。
2. Service层
Service层是业务逻辑的核心,所有和业务规则相关的异常都应该在这里抛出:
- 比如用户余额不足无法下单、订单状态不允许修改、传入的参数不符合业务规则(比如用户年龄不能小于18岁),这些场景都要在这里抛出异常。
- 这里可以抛自定义业务异常,也可以用Spring的
Assert工具类简化代码,比如:
不满足条件时会自动抛出Assert.isTrue(user.getBalance() >= orderAmount, "用户余额不足,无法完成下单");IllegalArgumentException。 - 另外,Repository层抛上来的数据库异常,建议在Service层包装成业务相关的异常,比如把
EmptyResultDataAccessException包装成UserNotFoundException,这样上层Controller不用关心数据库细节,只需要处理业务语义明确的异常。
3. Controller层
Controller层的职责是接收请求、调用Service、返回响应,这里尽量不要主动抛业务异常:
- 它的核心工作是处理Service层抛上来的各种异常,把异常转化为符合HTTP规范的响应(比如400 Bad Request、404 Not Found、500 Internal Server Error)。
- 对于请求参数的校验异常(比如
@Valid注解校验不通过抛出的MethodArgumentNotValidException),也建议在这里或全局处理器中统一处理,返回友好的参数错误提示。
二、除了自定义异常,还有哪些实用的异常处理方式?
自定义异常确实是业务异常处理的利器,但Spring生态还提供了很多现成的工具和方式,能帮我们减少重复代码:
利用Spring自带的通用异常:
Spring已经为大部分通用场景定义了异常,比如:EntityNotFoundException:JPA中查询实体不存在时抛出MethodArgumentNotValidException:请求参数校验不通过时抛出HttpRequestMethodNotSupportedException:请求方法不匹配时抛出
这些异常覆盖了很多通用场景,不用自己重复定义类似的自定义异常。
全局异常处理器:
用@RestControllerAdvice(针对REST接口)或@ControllerAdvice配合@ExceptionHandler实现全局异常统一处理,这是Spring项目中最常用的方式,不用在每个Controller里写重复的try-catch。
举个简单的实现示例:import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); // 处理自定义业务异常 @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) { logger.warn("业务异常触发:{}", e.getMessage()); ErrorResponse response = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage()); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } // 处理请求参数校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) { BindingResult result = e.getBindingResult(); String errorMsg = result.getFieldErrors().stream() .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) .collect(Collectors.joining("; ")); logger.warn("参数校验失败:{}", errorMsg); ErrorResponse response = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), errorMsg); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } // 处理系统级异常 @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleSystemException(Exception e) { logger.error("系统未知异常", e); ErrorResponse response = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误,请稍后重试"); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } // 内部错误响应类 static class ErrorResponse { private int code; private String message; public ErrorResponse(int code, String message) { this.code = code; this.message = message; } // getter、setter 省略 } }参数校验注解:
用JSR-380的校验注解(比如@NotNull、@Size(min = 6)、@Email)配合@Valid/@Validated注解,自动完成请求参数的合法性校验,Spring会在参数不合法时抛出MethodArgumentNotValidException,不用自己写一堆if-else判断抛异常。
示例:@PostMapping("/users") public ResponseEntity<User> createUser(@RequestBody @Valid UserCreateRequest request) { User user = userService.createUser(request); return ResponseEntity.ok(user); } // 请求参数类 static class UserCreateRequest { @NotNull(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间") private String username; @Email(message = "邮箱格式不正确") private String email; // getter、setter 省略 }断言工具类:
Spring提供的org.springframework.util.Assert类,能简化参数或状态校验的代码,比如:public void updateOrderStatus(Long orderId, Integer newStatus) { Order order = orderRepository.findById(orderId).orElseThrow(() -> new OrderNotFoundException("订单不存在")); // 断言订单当前状态允许修改 Assert.isTrue(order.getStatus().equals(OrderStatus.PENDING), "只有待支付订单才能修改状态"); // 后续业务逻辑 }不满足条件时会直接抛出对应的RuntimeException,比手动写if-else抛异常简洁很多。
三、最后再总结下核心思路
- 异常要分层职责清晰:Service管业务异常,Repository管数据库底层异常,Controller管异常转响应。
- 自定义异常用于业务语义明确的场景,通用场景优先用Spring自带的异常。
- 全局异常处理器是减少重复代码、统一响应格式的最佳实践,一定要用起来!
如果还有具体场景的疑问,比如某个特殊业务该怎么处理异常,随时再问~




