SpringBoot全局异常处理

 

Java Spring Boot 全局异常的优雅处理方式

在Java Spring Boot应用中,优雅地处理全局异常是构建健壮、用户友好的Web应用的重要部分。通过全局异常处理,可以统一管理异常响应,避免重复的try-catch代码块,提高代码可维护性和一致性。本文详细介绍Spring Boot中全局异常处理的优雅实现方式,包括背景、实现步骤、代码示例、进阶技巧、最佳实践和单元测试。


1. 全局异常处理的背景和意义

在Spring Boot应用中,异常可能来自以下场景:

  • 控制器(Controller)中的业务逻辑
  • 服务层(Service)的处理
  • 数据访问层(Repository)的数据库操作
  • 外部API调用或用户输入验证失败

如果每个方法或控制器都单独处理异常,会导致代码冗余、难以维护。全局异常处理通过集中式方式捕获和处理异常,提供统一的错误响应格式(如JSON),并支持自定义错误码、错误信息和HTTP状态码。

Spring Boot提供了@ControllerAdvice@ExceptionHandler注解,用于实现全局异常处理。这种方式可以:

  • 统一异常响应的格式(如返回标准的错误JSON)
  • 提高代码的可读性和可维护性
  • 提供友好的错误提示,增强用户体验
  • 便于日志记录和错误追踪

2. 实现全局异常处理的核心步骤

以下是实现全局异常处理的详细步骤。

2.1 定义统一的错误响应格式

为了让API返回一致的错误响应,通常会定义一个通用的错误响应类。例如:

public class ApiErrorResponse {
    private String errorCode; // 错误码
    private String message;   // 错误信息
    private int status;       // HTTP状态码
    private String timestamp; // 错误发生时间
    private String path;      // 请求路径

    public ApiErrorResponse(String errorCode, String message, int status, String path) {
        this.errorCode = errorCode;
        this.message = message;
        this.status = status;
        this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        this.path = path;
    }

    // Getters and Setters
}

这个类定义了API错误的结构,包含错误码、消息、状态码、时间戳和请求路径,便于前端解析和调试。

2.2 创建自定义异常

为了更好地分类和处理异常,可以定义一些自定义异常类。例如:

// 业务异常
public class BusinessException extends RuntimeException {
    private String errorCode;

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

    public String getErrorCode() {
        return errorCode;
    }
}

// 资源未找到异常
public class ResourceNotFoundException extends RuntimeException {
    private String errorCode;

    public ResourceNotFoundException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

自定义异常可以携带特定的错误码和消息,方便在全局异常处理中根据异常类型返回不同的响应。

2.3 使用@ControllerAdvice实现全局异常处理

@ControllerAdvice是一个增强型的Controller注解,专门用于处理全局异常。它可以捕获所有控制器抛出的异常,并通过@ExceptionHandler定义具体的异常处理逻辑。

以下是一个全局异常处理器的示例:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

import javax.validation.ConstraintViolationException;

@ControllerAdvice
public class GlobalExceptionHandler {

    // 处理自定义的业务异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiErrorResponse> handleBusinessException(BusinessException ex, WebRequest request) {
        ApiErrorResponse errorResponse = new ApiErrorResponse(
                ex.getErrorCode(),
                ex.getMessage(),
                HttpStatus.BAD_REQUEST.value(),
                request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

    // 处理资源未找到异常
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ApiErrorResponse errorResponse = new ApiErrorResponse(
                ex.getErrorCode(),
                ex.getMessage(),
                HttpStatus.NOT_FOUND.value(),
                request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    // 处理参数校验异常(如@Valid注解触发的异常)
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ApiErrorResponse> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
        String message = ex.getConstraintViolations().stream()
                .map(violation -> violation.getMessage())
                .collect(Collectors.joining("; "));
        ApiErrorResponse errorResponse = new ApiErrorResponse(
                "VALIDATION_ERROR",
                message,
                HttpStatus.BAD_REQUEST.value(),
                request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

    // 处理其他未捕获的异常(兜底)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
        ApiErrorResponse errorResponse = new ApiErrorResponse(
                "INTERNAL_SERVER_ERROR",
                "An unexpected error occurred",
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                request.getDescription(false)
        );
        // 记录日志,便于排查问题
        log.error("Unexpected error occurred: ", ex);
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

代码说明

  • @ControllerAdvice:标记该类为全局异常处理器,作用于所有控制器。
  • @ExceptionHandler:指定处理的异常类型(如BusinessExceptionResourceNotFoundException)。
  • WebRequest:提供请求上下文信息,如请求路径。
  • ResponseEntity:用于返回HTTP状态码和响应体。
  • 每种异常对应一个处理方法,返回统一的ApiErrorResponse对象。
  • 兜底的Exception处理方法捕获所有未明确处理的异常,防止系统崩溃。

2.4 日志记录

在全局异常处理器中,建议集成日志框架(如SLF4J+Logback)记录异常信息,便于问题排查。例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // ... 异常处理方法中添加日志
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
        log.error("Unexpected error occurred: ", ex);
        // ...
    }
}

2.5 示例控制器抛出异常

以下是一个控制器示例,展示如何抛出自定义异常:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public String getUser(@PathVariable Long id) {
        if (id <= 0) {
            throw new BusinessException("INVALID_ID", "User ID must be positive");
        }
        if (id == 999) {
            throw new ResourceNotFoundException("USER_NOT_FOUND", "User not found with ID: " + id);
        }
        return "User found: " + id;
    }
}

2.6 测试异常响应

假设调用/users/0,返回的JSON响应可能是:

{
  "errorCode": "INVALID_ID",
  "message": "User ID must be positive",
  "status": 400,
  "timestamp": "2025-04-30T10:00:00",
  "path": "uri=/users/0"
}

调用/users/999,返回:

{
  "errorCode": "USER_NOT_FOUND",
  "message": "User not found with ID: 999",
  "status": 404,
  "timestamp": "2025-04-30T10:00:00",
  "path": "uri=/users/999"
}

3. 优雅处理的进阶技巧

为了让全局异常处理更加优雅和灵活,可以考虑以下进阶技巧。

3.1 使用枚举管理错误码

将错误码和错误信息定义在枚举中,方便维护和复用。例如:

public enum ErrorCode {
    INVALID_ID("INVALID_ID", "User ID must be positive"),
    USER_NOT_FOUND("USER_NOT_FOUND", "User not found"),
    VALIDATION_ERROR("VALIDATION_ERROR", "Validation failed");

    private final String code;
    private final String message;

    ErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

然后在异常类中使用:

throw new BusinessException(ErrorCode.INVALID_ID.getCode(), ErrorCode.INVALID_ID.getMessage());

3.2 支持国际化(i18n)

如果应用需要支持多语言,可以使用Spring的MessageSource来实现错误信息的国际化。例如:

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;

@ControllerAdvice
public class GlobalExceptionHandler {
    private final MessageSource messageSource;

    public GlobalExceptionHandler(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiErrorResponse> handleBusinessException(BusinessException ex, WebRequest request) {
        String localizedMessage = messageSource.getMessage(ex.getErrorCode(), null, ex.getMessage(), LocaleContextHolder.getLocale());
        ApiErrorResponse errorResponse = new ApiErrorResponse(
                ex.getErrorCode(),
                localizedMessage,
                HttpStatus.BAD_REQUEST.value(),
                request.getDescription(false)
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

messages.properties中定义多语言错误信息:

INVALID_ID=User ID must be positive
USER_NOT_FOUND=User not found

3.3 处理方法参数校验异常

Spring Boot支持Bean Validation(如@NotNull@Size),当校验失败时会抛出MethodArgumentNotValidExceptionConstraintViolationException。可以专门处理这些异常:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) {
    String message = ex.getBindingResult().getFieldErrors().stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.joining("; "));
    ApiErrorResponse errorResponse = new ApiErrorResponse(
            "VALIDATION_ERROR",
            message,
            HttpStatus.BAD_REQUEST.value(),
            request.getDescription(false)
    );
    return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

3.4 异步异常处理

如果使用了@Async异步方法,异常不会被@ControllerAdvice直接捕获。需要在异步方法中手动捕获异常并抛出,或者使用AsyncUncaughtExceptionHandler

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;

public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(CustomAsyncExceptionHandler.class);

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        log.error("Async error in method {}: {}", method.getName(), ex.getMessage(), ex);
        // 可以将异常信息发送到消息队列或记录到数据库
    }
}

配置:

@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

3.5 集成监控和告警

对于生产环境,可以将异常信息发送到监控系统(如Sentry、ELK)或告警系统(如钉钉、Slack)。例如:

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
    // 记录日志
    log.error("Unexpected error: ", ex);
    // 发送到Sentry
    Sentry.captureException(ex);
    // 构造响应
    ApiErrorResponse errorResponse = new ApiErrorResponse(
            "INTERNAL_SERVER_ERROR",
            "An unexpected error occurred",
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            request.getDescription(false)
    );
    return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

4. 最佳实践

以下是一些实现全局异常处理的最佳实践:

  1. 统一响应格式:始终返回一致的错误响应结构(如ApiErrorResponse)。
  2. 明确异常类型:为不同的场景定义特定的异常类(如BusinessExceptionResourceNotFoundException)。
  3. 日志记录:记录详细的异常信息,包括堆栈跟踪,方便调试。
  4. 用户友好提示:对用户隐藏敏感信息(如堆栈跟踪),返回简洁的错误消息。
  5. HTTP状态码准确:根据异常类型选择合适的HTTP状态码(如404、400、500)。
  6. 测试覆盖:编写单元测试和集成测试,验证异常处理的正确性。
  7. 文档化:在API文档(如Swagger)中说明可能的错误码和错误信息。

5. 单元测试示例

为了确保全局异常处理逻辑正确,可以使用Spring Boot Test进行测试:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
public class GlobalExceptionHandlerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldReturnBadRequestWhenInvalidId() throws Exception {
        mockMvc.perform(get("/users/0"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorCode").value("INVALID_ID"))
                .andExpect(jsonPath("$.message").value("User ID must be positive"))
                .andExpect(jsonPath("$.status").value(400));
    }

    @Test
    public void shouldReturnNotFoundWhenUserNotFound() throws Exception {
        mockMvc.perform(get("/users/999"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.errorCode").value("USER_NOT_FOUND"))
                .andExpect(jsonPath("$.message").value("User not found with ID: 999"))
                .andExpect(jsonPath("$.status").value(404));
    }
}

6. 总结

通过Spring Boot的@ControllerAdvice@ExceptionHandler,可以实现优雅的全局异常处理。核心步骤包括:

  1. 定义统一的错误响应格式(如ApiErrorResponse
  2. 创建自定义异常类(如BusinessException
  3. 实现全局异常处理器,处理特定异常和兜底异常
  4. 集成日志、国际化、监控等功能,增强健壮性
  5. 遵循最佳实践,确保代码可维护和用户友好

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International