Spring异常处理:控制器层还是服务层?
解决方案:服务层抛出业务异常 + 全局统一处理
优先选择在服务层抛出特定业务异常,配合@RestControllerAdvice全局处理,这既不违反单一职责原则,也能实现精细化的错误响应,同时避免控制器代码杂乱或强制声明throws的问题。
核心思路解析
所谓“服务层抛异常是不良实践”的说法,指的是滥用宽泛的运行时异常,而非合理的业务异常。服务层作为业务逻辑的核心,校验权限、业务规则是它的职责——遇到不符合规则的场景,抛出明确的业务异常比返回Optional.empty()更能精准传递失败原因,也便于全局统一处理。
另外,自定义业务异常建议继承RuntimeException(非检查型异常),这样不需要在控制器方法上添加throws声明,Spring会自动捕获并交给全局异常处理器处理,代码更简洁。
具体改造步骤
1. 创建自定义业务异常类
针对不同失败场景定义专属异常,替代模糊的AccessDeniedException:
// 非项目所有者异常 public class NotProjectOwnerException extends RuntimeException { public NotProjectOwnerException(String message) { super(message); } } // 权限不足异常 public class InsufficientPermissionsException extends RuntimeException { public InsufficientPermissionsException(String message) { super(message); } }
2. 改造服务层方法,抛对应异常
移除Optional返回值,根据校验结果抛出特定异常:
public ProjectDTO changeOwner(Volunteer user, Project project) { Volunteer loggedVolunteer = volunteerService.getLoggedVolunteer(); boolean isOwner = this.isVolunteerProjectOwner(loggedVolunteer, project); boolean isAdmin = authenticationService.checkIfAdmin(loggedVolunteer); if (!isOwner && !isAdmin) { if (!isOwner) { throw new NotProjectOwnerException("仅项目所有者可变更负责人"); } else { throw new InsufficientPermissionsException("无权限执行此操作"); } } project.setOwnerVolunteer(user); projectRepository.save(project); return projectMapper.mapProjectToDTO(project); }
3. 全局异常处理器添加对应处理
在已有的@RestControllerAdvice中新增异常处理逻辑,返回标准化错误响应:
@RestControllerAdvice public class GlobalExceptionHandler { // 处理非项目所有者异常 @ExceptionHandler(NotProjectOwnerException.class) public ResponseEntity<ErrorResponse> handleNotProjectOwner(NotProjectOwnerException ex) { ErrorResponse error = new ErrorResponse(HttpStatus.FORBIDDEN.value(), ex.getMessage()); return new ResponseEntity<>(error, HttpStatus.FORBIDDEN); } // 处理权限不足异常 @ExceptionHandler(InsufficientPermissionsException.class) public ResponseEntity<ErrorResponse> handleInsufficientPermissions(InsufficientPermissionsException ex) { ErrorResponse error = new ErrorResponse(HttpStatus.FORBIDDEN.value(), ex.getMessage()); return new ResponseEntity<>(error, HttpStatus.FORBIDDEN); } // 保留原有EntityNotFoundException等异常的处理逻辑 @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity<ErrorResponse> handleEntityNotFound(EntityNotFoundException ex) { ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage()); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } } // 标准化错误响应DTO class ErrorResponse { private int code; private String message; // 构造方法、getter/setter省略 }
4. 简化控制器代码
控制器只需负责请求接收、参数校验和服务调用,无需处理业务失败逻辑:
@PatchMapping(value = "/{projectId}/change-owner/{volunteerId}") public ResponseEntity<ProjectDTO> changeProjectOwner(@PathVariable Long projectId, @PathVariable Long volunteerId) { Project foundProject = projectService.findProject(projectId) .orElseThrow(() -> new EntityNotFoundException("项目不存在")); Volunteer foundVolunteer = volunteerService.findVolunteer(volunteerId) .orElseThrow(() -> new EntityNotFoundException("志愿者不存在")); ProjectDTO projectDTO = projectService.changeOwner(foundVolunteer, foundProject); return ResponseEntity.ok(projectDTO); }
为什么不把校验放控制器?
控制器的核心职责是接收请求、参数格式校验、调用服务层、返回响应,把业务规则校验(如是否为项目所有者、是否为管理员)放在控制器会导致:
- 控制器代码臃肿,违反单一职责原则;
- 相同校验逻辑无法在多个端点复用,增加冗余代码;
- 业务逻辑分散,后续维护难度提升。
内容的提问来源于stack exchange,提问作者Thorvas
相关产品推荐
相关产品推荐

