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 或 null
↓
6. 若返回 null,继续调用下一个 Resolver
异常处理优先级:
- @ExceptionHandler 方法(同一类)
- @ControllerAdvice 中的 @ExceptionHandler 方法
- 默认异常处理器
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);
}
}