You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 14:15:23