Java全局异常处理器:优雅处理系统异常
Java全局异常处理器:优雅处理系统异常的最佳实践
概述
在Java Web应用开发中,异常处理是一个至关重要的环节。传统的异常处理方式往往需要在每个Controller方法中编写大量的try-catch代码,这不仅造成代码冗余,还降低了代码的可读性和可维护性。全局异常处理器(Global Exception Handler)应运而生,它能够统一处理整个应用程序中抛出的异常,让我们的代码更加简洁和优雅。
什么是全局异常处理器?
全局异常处理器是Spring框架提供的一种统一异常处理机制,通过@ControllerAdvice
和@ExceptionHandler
注解的组合使用,能够捕获并处理整个应用程序中抛出的各种异常。它的核心思想是将异常处理逻辑从业务代码中分离出来,集中在专门的异常处理类中进行管理。
工作原理
1. 异常传播机制
当应用程序中发生异常时,异常会沿着调用栈向上传播:
Controller Layer → Service Layer → Repository Layer↓
Exception Handler (全局异常处理器)↓
统一的错误响应
2. 注解工作原理
- @ControllerAdvice:标识这是一个全局异常处理类,Spring会自动扫描并注册
- @ExceptionHandler:指定该方法处理哪种类型的异常
- @ResponseBody:将返回值序列化为JSON格式(或配合@RestControllerAdvice使用)
核心组件解析
异常处理优先级
Spring的异常处理遵循就近原则:
- Controller内部的@ExceptionHandler
- @ControllerAdvice中的@ExceptionHandler
- 默认的异常处理机制
异常匹配规则
当异常发生时,Spring会按照以下规则选择合适的异常处理器:
- 精确匹配异常类型
- 匹配父类异常类型
- 匹配接口异常类型
完整Demo实现
1. 自定义异常类
package com.example.exception;/*** 业务异常基类*/
public class BusinessException extends RuntimeException {private final String code;private final String message;public BusinessException(String code, String message) {super(message);this.code = code;this.message = message;}public BusinessException(String code, String message, Throwable cause) {super(message, cause);this.code = code;this.message = message;}public String getCode() {return code;}@Overridepublic String getMessage() {return message;}
}/*** 资源未找到异常*/
public class ResourceNotFoundException extends BusinessException {public ResourceNotFoundException(String resource) {super("RESOURCE_NOT_FOUND", "资源未找到: " + resource);}
}/*** 参数校验异常*/
public class ValidationException extends BusinessException {public ValidationException(String message) {super("VALIDATION_ERROR", message);}
}/*** 权限不足异常*/
public class AccessDeniedException extends BusinessException {public AccessDeniedException() {super("ACCESS_DENIED", "权限不足,拒绝访问");}
}
2. 统一响应结果类
package com.example.common;import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;/*** 统一响应结果封装*/
public class ApiResponse<T> {private String code;private String message;private T data;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime timestamp;public ApiResponse() {this.timestamp = LocalDateTime.now();}public ApiResponse(String code, String message) {this();this.code = code;this.message = message;}public ApiResponse(String code, String message, T data) {this(code, message);this.data = data;}// 成功响应public static <T> ApiResponse<T> success(T data) {return new ApiResponse<>("SUCCESS", "操作成功", data);}public static <T> ApiResponse<T> success(String message, T data) {return new ApiResponse<>("SUCCESS", message, data);}// 失败响应public static <T> ApiResponse<T> error(String code, String message) {return new ApiResponse<>(code, message);}public static <T> ApiResponse<T> error(String message) {return new ApiResponse<>("ERROR", message);}// Getters and Setterspublic String getCode() { return code; }public void setCode(String code) { this.code = code; }public String getMessage() { return message; }public void setMessage(String message) { this.message = message; }public T getData() { return data; }public void setData(T data) { this.data = data; }public LocalDateTime getTimestamp() { return timestamp; }public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
3. 全局异常处理器
package com.example.handler;import com.example.common.ApiResponse;
import com.example.exception.BusinessException;
import com.example.exception.ResourceNotFoundException;
import com.example.exception.ValidationException;
import com.example.exception.AccessDeniedException;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;/*** 全局异常处理器*/
@RestControllerAdvice
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 处理业务异常*/@ExceptionHandler(BusinessException.class)public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e, WebRequest request) {logger.warn("业务异常: {}", e.getMessage());ApiResponse<Void> response = ApiResponse.error(e.getCode(), e.getMessage());return ResponseEntity.badRequest().body(response);}/*** 处理资源未找到异常*/@ExceptionHandler(ResourceNotFoundException.class)public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(ResourceNotFoundException e, WebRequest request) {logger.warn("资源未找到: {}", e.getMessage());ApiResponse<Void> response = ApiResponse.error(e.getCode(), e.getMessage());return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);}/*** 处理权限异常*/@ExceptionHandler(AccessDeniedException.class)public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException e, WebRequest request) {logger.warn("权限不足: {}", e.getMessage());ApiResponse<Void> response = ApiResponse.error(e.getCode(), e.getMessage());return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);}/*** 处理参数校验异常 - @Valid注解校验失败*/@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e, WebRequest request) {logger.warn("参数校验失败: {}", e.getMessage());String errorMessage = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining("; "));ApiResponse<Void> response = ApiResponse.error("VALIDATION_ERROR", errorMessage);return ResponseEntity.badRequest().body(response);}/*** 处理参数绑定异常*/@ExceptionHandler(BindException.class)public ResponseEntity<ApiResponse<Void>> handleBindException(BindException e, WebRequest request) {logger.warn("参数绑定异常: {}", e.getMessage());String errorMessage = e.getBindingResult().getFieldErrors().stream().map(error -> error.getField() + ": " + error.getDefaultMessage()).collect(Collectors.joining("; "));ApiResponse<Void> response = ApiResponse.error("BIND_ERROR", errorMessage);return ResponseEntity.badRequest().body(response);}/*** 处理约束违规异常*/@ExceptionHandler(ConstraintViolationException.class)public ResponseEntity<ApiResponse<Void>> handleConstraintViolationException(ConstraintViolationException e, WebRequest request) {logger.warn("约束违规: {}", e.getMessage());String errorMessage = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("; "));ApiResponse<Void> response = ApiResponse.error("CONSTRAINT_VIOLATION", errorMessage);return ResponseEntity.badRequest().body(response);}/*** 处理参数类型不匹配异常*/@ExceptionHandler(MethodArgumentTypeMismatchException.class)public ResponseEntity<ApiResponse<Void>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, WebRequest request) {logger.warn("参数类型不匹配: {}", e.getMessage());String errorMessage = String.format("参数 '%s' 的值 '%s' 类型不正确,期望类型: %s",e.getName(), e.getValue(), e.getRequiredType().getSimpleName());ApiResponse<Void> response = ApiResponse.error("TYPE_MISMATCH", errorMessage);return ResponseEntity.badRequest().body(response);}/*** 处理IllegalArgumentException*/@ExceptionHandler(IllegalArgumentException.class)public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException e, WebRequest request) {logger.warn("非法参数: {}", e.getMessage());ApiResponse<Void> response = ApiResponse.error("ILLEGAL_ARGUMENT", e.getMessage());return ResponseEntity.badRequest().body(response);}/*** 处理其他未捕获的异常*/@ExceptionHandler(Exception.class)public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e, WebRequest request) {logger.error("系统异常: ", e);ApiResponse<Void> response = ApiResponse.error("INTERNAL_ERROR", "系统内部错误,请联系管理员");return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}
}
4. 业务Service层示例
package com.example.service;import com.example.exception.ResourceNotFoundException;
import com.example.exception.ValidationException;
import org.springframework.stereotype.Service;/*** 用户服务示例*/
@Service
public class UserService {public String getUserById(Long id) {if (id == null || id <= 0) {throw new ValidationException("用户ID不能为空或小于等于0");}if (id == 999L) {throw new ResourceNotFoundException("用户ID: " + id);}return "用户信息: ID=" + id;}public void deleteUser(Long id) {if (id == null) {throw new ValidationException("用户ID不能为空");}if (id == 1L) {throw new IllegalArgumentException("不能删除管理员用户");}// 模拟删除逻辑System.out.println("删除用户: " + id);}
}
5. Controller层示例
package com.example.controller;import com.example.common.ApiResponse;
import com.example.exception.AccessDeniedException;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;/*** 用户控制器示例*/
@RestController
@RequestMapping("/api/users")
public class UserController {@Autowiredprivate UserService userService;/*** 获取用户信息*/@GetMapping("/{id}")public ApiResponse<String> getUser(@PathVariable @NotNull @Min(value = 1, message = "用户ID必须大于0") Long id) {String user = userService.getUserById(id);return ApiResponse.success(user);}/*** 删除用户*/@DeleteMapping("/{id}")public ApiResponse<Void> deleteUser(@PathVariable Long id) {userService.deleteUser(id);return ApiResponse.success("删除成功", null);}/*** 管理员操作示例*/@PostMapping("/admin/reset")public ApiResponse<Void> adminReset(@RequestParam String action) {// 模拟权限检查boolean isAdmin = false; // 假设当前用户不是管理员if (!isAdmin) {throw new AccessDeniedException();}return ApiResponse.success("重置成功", null);}/*** 模拟系统异常*/@GetMapping("/error")public ApiResponse<Void> triggerError() {// 故意抛出运行时异常throw new RuntimeException("模拟系统异常");}
}
最佳实践
1. 异常分层设计
// 按业务模块划分异常
public class UserException extends BusinessException {public UserException(String message) {super("USER_ERROR", message);}
}public class OrderException extends BusinessException {public OrderException(String message) {super("ORDER_ERROR", message);}
}
2. 日志记录策略
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) {// 业务异常记录为WARN级别logger.warn("业务异常 [{}]: {}", e.getCode(), e.getMessage());// 系统异常记录为ERROR级别,包含堆栈信息// logger.error("系统异常: ", e);
}
3. 异常信息国际化
@Component
public class MessageHelper {@Autowiredprivate MessageSource messageSource;public String getMessage(String code, Object... args) {return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());}
}
测试验证
使用以下curl命令测试各种异常情况:
# 1. 正常请求
curl http://localhost:8080/api/users/1# 2. 参数校验异常
curl http://localhost:8080/api/users/0# 3. 资源未找到异常
curl http://localhost:8080/api/users/999# 4. 权限不足异常
curl -X POST http://localhost:8080/api/users/admin/reset?action=reset# 5. 系统异常
curl http://localhost:8080/api/users/error
总结
全局异常处理器是Spring Boot应用中不可或缺的组件,它具有以下优势:
- 代码解耦:将异常处理逻辑从业务代码中分离
- 统一响应:确保所有异常都以统一的格式返回
- 便于维护:集中管理异常处理逻辑,便于修改和扩展
- 提升用户体验:提供友好的错误信息
- 便于监控:统一的异常记录便于系统监控和问题排查
通过合理使用全局异常处理器,我们可以构建更加健壮、可维护的Java Web应用程序。记住,好的异常处理不仅能够提升代码质量,更能为用户提供更好的使用体验。