3-3 全局异常处理

0 阅读4分钟

3-3 全局异常处理

概念解析

异常处理注解

注解说明
@ControllerAdvice标记全局异常处理类
@RestControllerAdvice@ControllerAdvice + @ResponseBody
@ExceptionHandler处理特定异常

异常分类

类型说明处理方式
业务异常自定义业务逻辑异常友好提示
参数校验异常@Validated 校验失败返回校验信息
认证授权异常未登录/无权限返回 401/403
系统异常数据库/IO 异常记录日志,友好提示
第三方异常调用外部服务失败降级处理

代码示例

1. 基本全局异常处理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统繁忙,请稍后重试");
    }
}

2. 自定义业务异常

// 基础业务异常
@Data
public class BusinessException extends RuntimeException {

    private int code = 400;
    private String message;

    public BusinessException(String message) {
        super(message);
        this.message = message;
    }

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
}

// 预定义异常
public class BizException {

    public static BusinessException notFound(String resource) {
        return new BusinessException(404, resource + " 不存在");
    }

    public static BusinessException forbidden(String message) {
        return new BusinessException(403, message);
    }

    public static BusinessException unauthorized() {
        return new BusinessException(401, "未登录或登录已过期");
    }

    public static BusinessException badRequest(String message) {
        return new BusinessException(400, message);
    }
}

// 使用
@Service
public class UserService {

    public User getById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> BizException.notFound("用户"));
    }

    public void delete(Long id) {
        if (!hasPermission(id)) {
            throw BizException.forbidden("无权限删除该用户");
        }
        userRepository.deleteById(id);
    }
}

3. 参数校验异常处理

@RestControllerAdvice
@Slf4j
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<List<FieldError>> handleValidException(
            MethodArgumentNotValidException e) {

        List<FieldError> errors = e.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> {
                FieldError fieldError = new FieldError();
                fieldError.setField(error.getField());
                fieldError.setMessage(error.getDefaultMessage());
                return fieldError;
            })
            .collect(Collectors.toList());

        return Result.error(400, "参数校验失败").setData(errors);
    }

    @ExceptionHandler(BindException.class)
    public Result<Void> handleBindException(BindException e) {
        String message = e.getBindingResult().getFieldErrors()
            .stream()
            .map(FieldError::getDefaultMessage)
            .collect(Collectors.joining(", "));

        return Result.error(400, message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public Result<Void> handleConstraintViolation(
            ConstraintViolationException e) {

        String message = e.getConstraintViolations()
            .stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.joining(", "));

        return Result.error(400, message);
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Result<Void> handleMissingParam(
            MissingServletRequestParameterException e) {

        return Result.error(400, "缺少参数: " + e.getParameterName());
    }
}

4. 统一错误页面处理

@RestControllerAdvice
public class ErrorController {

    @RequestMapping("/error")
    public Result<Void> error(HttpServletRequest request, Exception e) {
        Integer status = (Integer) request.getAttribute(
            "jakarta.servlet.error.status_code");

        if (status == null) {
            status = 500;
        }

        String message;
        switch (status) {
            case 400:
                message = "请求参数错误";
                break;
            case 401:
                message = "未登录或登录已过期";
                break;
            case 403:
                message = "无权限访问";
                break;
            case 404:
                message = "请求的资源不存在";
                break;
            case 500:
                message = "系统繁忙,请稍后重试";
                break;
            default:
                message = "未知错误";
        }

        return Result.error(status, message);
    }
}

5. 异常处理配置类

@Configuration
public class ExceptionHandlerConfig {

    @Bean
    public ErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes() {
            @Override
            public Map<String, Object> getErrorAttributes(
                    RequestAttributes requestAttributes,
                    boolean includeStackTrace) {

                Map<String, Object> errorAttributes = new LinkedHashMap<>();
                errorAttributes.put("timestamp", new Date());
                errorAttributes.put("status", 500);

                Throwable error = getError(requestAttributes);
                if (error != null) {
                    errorAttributes.put("message", error.getMessage());
                }

                return errorAttributes;
            }
        };
    }
}

常见坑点

⚠️ 坑 1:异常被 catch 后不抛

@Service
public class UserService {

    // ❌ 异常被吞掉
    public void createUser(User user) {
        try {
            validateUser(user);
            userRepository.save(user);
        } catch (Exception e) {
            // 什么都不做,事务不会回滚
        }
    }

    // ✅ 正确做法
    public void createUser(User user) {
        validateUser(user);
        userRepository.save(user);
    }

    // 或
    public void createUser(User user) {
        try {
            validateUser(user);
            userRepository.save(user);
        } catch (Exception e) {
            throw new RuntimeException("创建用户失败", e);  // 重新抛出
        }
    }
}

⚠️ 坑 2:Controller 层捕获异常

// ❌ 不要在 Controller 中捕获异常
@PostMapping
public Result<Void> create(@RequestBody UserDTO dto) {
    try {
        userService.create(dto);
        return Result.success(null);
    } catch (Exception e) {
        return Result.error(e.getMessage());
    }
}

// ✅ 让异常传播到全局处理器
@PostMapping
public Result<Void> create(@RequestBody UserDTO dto) {
    userService.create(dto);  // 让异常自然抛出
    return Result.success(null);
}

⚠️ 坑 3:异步方法异常处理

@Service
public class AsyncService {

    @Async
    public void asyncTask() {
        // 异常不会传播到调用方
        throw new RuntimeException("异步异常");
    }

    // ✅ 使用 Future 处理
    @Async
    public Future<String> asyncTaskWithResult() {
        try {
            // 业务逻辑
            return AsyncResult.forSuccess("success");
        } catch (Exception e) {
            return AsyncResult.forFailure(e);
        }
    }
}

面试题

Q1:Spring MVC 的异常处理流程?

参考答案

1. Controller 抛出异常
         ↓
2. DispatcherServlet.processHandlerException()
         ↓
3. 遍历 HandlerExceptionResolvers
         ↓
4. @ExceptionHandler 处理
         ↓
5. 返回 ModelAndView 或 null6. 若返回 null,继续调用下一个 Resolver

异常处理优先级

  1. @ExceptionHandler 方法(同一类)
  2. @ControllerAdvice 中的 @ExceptionHandler 方法
  3. 默认异常处理器

Q2:@ControllerAdvice 的作用范围?

参考答案

// 1. 全局处理(默认)
@RestControllerAdvice
public class GlobalHandler { }

// 2. 指定包
@RestControllerAdvice(basePackages = "com.example.demo.controller")
public class PackageHandler { }

// 3. 指定类
@RestControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
public class SpecificHandler { }

// 4. 指定注解
@RestControllerAdvice(annotations = RestController.class)
public class AnnotatedHandler { }

Q3:如何实现自定义异常国际化?

参考答案

// 1. 定义异常消息属性文件
// i18n/messages_zh_CN.properties
user.not.found=用户不存在
user.forbidden=无权限访问该用户

// i18n/messages_en.properties
user.not.found=User not found
user.forbidden=Access denied

// 2. 异常中使用消息码
public class BusinessException extends RuntimeException {
    private final String code;

    public BusinessException(String code) {
        this.code = code;
    }

    public String getLocalizedMessage(Locale locale) {
        return messageSource.getMessage(code, null, locale);
    }
}

// 3. 全局处理器使用
@RestControllerAdvice
public class GlobalExceptionHandler {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(BusinessException.class)
    public Result<Void> handle(BusinessException e, HttpServletRequest request) {
        Locale locale = request.getLocale();
        String message = e.getLocalizedMessage(locale);
        return Result.error(e.getCode(), message);
    }
}