前言
异常处理是 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));
}
}
总结
统一异常处理要点:
- ✅ 异常体系 - 自定义异常基类、业务异常、错误码
- ✅ @ControllerAdvice - 全局异常处理
- ✅ @ExceptionHandler - 处理方法
- ✅ 错误页面 - 自定义错误响应
- ✅ 日志记录 - 记录异常信息
- ✅ 最佳实践 - 异常分层、不吞异常、使用 Optional
良好的异常处理机制,能提升系统的健壮性和用户体验。