Skip to content
清晨的一缕阳光
返回

Spring Boot 统一异常处理

前言

异常处理是 Web 开发中不可或缺的一环。良好的异常处理机制能提供友好的错误提示,便于问题排查。Spring Boot 提供了@ControllerAdvice 和@ExceptionHandler 等强大的异常处理机制。

异常体系设计

1. 自定义异常基类

package com.example.demo.exception;

import lombok.Getter;

@Getter
public abstract class BaseException extends RuntimeException {
    
    private final String code;
    private final String message;
    
    public BaseException(String code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
    
    public BaseException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
}

2. 业务异常

package com.example.demo.exception;

public class BusinessException extends BaseException {
    
    public BusinessException(String code, String message) {
        super(code, message);
    }
    
    public BusinessException(String code, String message, Throwable cause) {
        super(code, message, cause);
    }
    
    // 常用快捷方法
    public static BusinessException notFound(String resource) {
        return new BusinessException("NOT_FOUND", resource + "不存在");
    }
    
    public static BusinessException badRequest(String message) {
        return new BusinessException("BAD_REQUEST", message);
    }
    
    public static BusinessException unauthorized() {
        return new BusinessException("UNAUTHORIZED", "未授权访问");
    }
    
    public static BusinessException forbidden() {
        return new BusinessException("FORBIDDEN", "禁止访问");
    }
}

3. 错误码设计

package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ErrorCode {
    
    SUCCESS(200, "成功"),
    
    // 客户端错误 4xx
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未授权"),
    FORBIDDEN(403, "禁止访问"),
    NOT_FOUND(404, "资源不存在"),
    METHOD_NOT_ALLOWED(405, "方法不允许"),
    CONFLICT(409, "资源冲突"),
    
    // 服务端错误 5xx
    INTERNAL_ERROR(500, "服务器内部错误"),
    SERVICE_UNAVAILABLE(503, "服务不可用"),
    
    // 业务错误
    USER_NOT_FOUND(1001, "用户不存在"),
    USER_ALREADY_EXISTS(1002, "用户已存在"),
    PASSWORD_ERROR(1003, "密码错误"),
    TOKEN_EXPIRED(1004, "Token 已过期"),
    TOKEN_INVALID(1005, "Token 无效");
    
    private final int code;
    private final String message;
}

统一异常处理

1. @ControllerAdvice

package com.example.demo.handler;

import com.example.demo.common.ErrorCode;
import com.example.demo.common.R;
import com.example.demo.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
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 org.springframework.web.servlet.NoHandlerFoundException;

import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<R<Void>> handleBusinessException(BusinessException e) {
        log.warn("业务异常:{}", e.getMessage());
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(R.error(e.getCode(), e.getMessage()));
    }
    
    /**
     * 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<R<Void>> handleValidationException(
        MethodArgumentNotValidException e
    ) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining("; "));
        
        log.warn("参数校验失败:{}", message);
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(R.error(ErrorCode.BAD_REQUEST.getCode(), message));
    }
    
    /**
     * 绑定异常
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity<R<Void>> handleBindException(BindException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining("; "));
        
        log.warn("参数绑定失败:{}", message);
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(R.error(ErrorCode.BAD_REQUEST.getCode(), message));
    }
    
    /**
     * 资源不存在
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<R<Void>> handleNotFound(NoHandlerFoundException e) {
        log.warn("资源不存在:{}", e.getRequestURL());
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(R.error(ErrorCode.NOT_FOUND.getCode(), "资源不存在"));
    }
    
    /**
     * 其他异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<R<Void>> handleException(Exception e) {
        log.error("系统异常", e);
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(R.error(ErrorCode.INTERNAL_ERROR.getCode(), "系统繁忙,请稍后重试"));
    }
}

2. 使用示例

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    @GetMapping("/{id}")
    public R<UserDTO> getUser(@PathVariable Long id) {
        UserDTO user = userService.findById(id);
        if (user == null) {
            throw BusinessException.notFound("用户");
        }
        return R.ok(user);
    }
    
    @PostMapping
    public R<UserDTO> createUser(@Valid @RequestBody UserCreateDTO dto) {
        // 参数校验失败会抛出 MethodArgumentNotValidException
        boolean exists = userService.existsByUsername(dto.getUsername());
        if (exists) {
            throw new BusinessException(
                ErrorCode.USER_ALREADY_EXISTS.getCode(),
                ErrorCode.USER_ALREADY_EXISTS.getMessage()
            );
        }
        return R.ok(userService.create(dto));
    }
}

自定义异常处理

1. 按状态码处理

@RestControllerAdvice
public class StatusCodeExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public R<Void> handleUserNotFound(UserNotFoundException e) {
        return R.error(404, e.getMessage());
    }
    
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public R<Void> handleUnauthorized(UnauthorizedException e) {
        return R.error(401, e.getMessage());
    }
    
    @ExceptionHandler(ForbiddenException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public R<Void> handleForbidden(ForbiddenException e) {
        return R.error(403, e.getMessage());
    }
}

2. 按包路径处理

@RestControllerAdvice("com.example.demo.controller")
public class PackageExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public R<Void> handleException(Exception e) {
        return R.error(500, "系统异常");
    }
}

3. 多@ControllerAdvice

// Web 异常处理
@RestControllerAdvice(assignableTypes = {
    UserController.class,
    OrderController.class
})
public class WebExceptionHandler {
    // ...
}

// API 异常处理
@RestControllerAdvice(basePackages = "com.example.demo.api")
public class ApiExceptionHandler {
    // ...
}

错误页面配置

1. 自定义错误页面

@Configuration
public class ErrorPageConfig implements ErrorController {
    
    @RequestMapping("/error")
    public ResponseEntity<R<Void>> handleError(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute(
            RequestDispatcher.ERROR_STATUS_CODE
        );
        String message = (String) request.getAttribute(
            RequestDispatcher.ERROR_MESSAGE
        );
        
        return ResponseEntity
            .status(statusCode != null ? statusCode : 500)
            .body(R.error(statusCode != null ? statusCode : 500, message));
    }
}

2. 配置错误属性

server:
  error:
    path: /error
    whitelabel:
      enabled: false  # 禁用默认错误页面
  include-exception: false
  include-stacktrace: never
  include-message: always

异常日志记录

1. 配置日志

@RestControllerAdvice
@Slf4j
public class LoggingExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public R<Void> handleException(Exception e) {
        // 记录完整堆栈
        log.error("系统异常", e);
        
        // 或者只记录消息
        log.error("系统异常:{}", e.getMessage());
        
        return R.error(500, "系统繁忙");
    }
}

2. 配置日志级别

logging:
  level:
    com.example.demo: DEBUG
    com.example.demo.handler: DEBUG
    org.springframework.web: INFO

最佳实践

1. 异常分层

// 基础异常
public abstract class BaseException extends RuntimeException {
    // ...
}

// 业务异常
public class BusinessException extends BaseException {
    // ...
}

// 技术异常
public class TechnicalException extends BaseException {
    // ...
}

// 具体业务异常
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException() {
        super("USER_NOT_FOUND", "用户不存在");
    }
}

2. 不要吞掉异常

// ❌ 不推荐
try {
    // 业务逻辑
} catch (Exception e) {
    log.error("错误", e);
    // 没有抛出异常,调用方不知道失败了
}

// ✅ 推荐
try {
    // 业务逻辑
} catch (Exception e) {
    log.error("错误", e);
    throw new BusinessException("操作失败", e);
}

3. 使用 Optional

// ❌ 不推荐
User user = userService.findById(id);
if (user == null) {
    throw BusinessException.notFound("用户");
}

// ✅ 推荐
User user = userService.findById(id)
    .orElseThrow(() -> BusinessException.notFound("用户"));

4. 异常信息国际化

@Configuration
public class MessageConfig {
    
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
        source.setBasename("classpath:messages");
        source.setDefaultEncoding("UTF-8");
        return source;
    }
}
# messages.properties
user.not.found=用户不存在
user.already.exists=用户已存在
password.error=密码错误
public class BusinessException extends BaseException {
    
    public BusinessException(String code, String message, Object... args) {
        super(code, MessageFormat.format(message, args));
    }
}

总结

统一异常处理要点:

良好的异常处理机制,能提升系统的健壮性和用户体验。


分享这篇文章到:

上一篇文章
Kafka 集成
下一篇文章
Spring Boot 邮件发送实战