JAVA Spring Boot 如何优雅实现全局异常与统一返回结构?

Spring Boot 全局异常处理与统一返回结构:一场优雅的邂逅

大家好,今天我们来聊聊在 Spring Boot 应用中,如何优雅地实现全局异常处理和统一返回结构。这是一个非常重要的课题,直接关系到 API 的健壮性、可维护性和用户体验。想象一下,你的 API 总是抛出各种各样的异常,返回的数据格式也五花八门,这简直就是一场噩梦。我们需要一套统一的机制,让异常处理变得可控,返回的数据结构清晰一致。

1. 为什么要全局异常处理和统一返回结构?

在开始之前,我们先明确一下为什么要这么做。

痛点 解决方案 收益
API 抛出各种异常,信息不统一 全局异常处理,将所有异常转化为统一的错误信息 提高 API 的可维护性,方便排查问题。前端开发者可以根据统一的错误码进行处理,减少沟通成本。
返回数据格式不一致,前后端对接困难 统一返回数据结构,包含状态码、消息、数据等字段 前后端分离更加彻底,前端开发者无需关心后端具体实现,只需要根据统一的接口协议进行开发。降低前后端耦合度,提高开发效率。
代码中散落着大量的 try-catch 块,冗余重复 将异常处理逻辑集中到一个地方,避免代码重复 提高代码的可读性和可维护性,减少代码量。
缺乏统一的错误码管理 定义一套清晰的错误码体系,方便定位问题 方便前后端沟通,快速定位问题。方便监控系统对异常进行统计分析。

总而言之,全局异常处理和统一返回结构是为了让我们的 API 更加健壮、易用和易维护。

2. 统一返回数据结构的设计

首先,我们需要定义一个通用的返回数据结构。这个结构应该包含以下几个要素:

  • code: 状态码,用于表示 API 的调用结果,例如 200 表示成功,500 表示服务器错误。
  • message: 消息,用于描述 API 的调用结果,例如 "操作成功","用户名不能为空"。
  • data: 数据,用于存放 API 返回的实际数据。

我们可以创建一个 Result 类来实现这个结构:

import lombok.Data;

@Data
public class Result<T> {

    private int code;
    private String message;
    private T data;

    private Result(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }

    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null);
    }

    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null);
    }

    public static <T> Result<T> error(int code, String message, T data) {
        return new Result<>(code, message, data);
    }

    public static <T> Result<T> error() {
        return new Result<>(500, "服务器错误", null);
    }
}

这个 Result 类使用了泛型,可以存放任意类型的数据。同时,我们提供了一些静态方法,方便创建不同状态的 Result 对象。例如 success(data) 用于创建成功返回结果,error(code, message) 用于创建错误返回结果。

3. 全局异常处理器的实现

接下来,我们需要创建一个全局异常处理器,用于捕获所有未处理的异常,并将它们转化为统一的错误信息。Spring Boot 提供了 @ControllerAdvice@ExceptionHandler 注解来实现这个功能。

import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handleException(Exception e) {
        e.printStackTrace(); // 打印异常信息,方便排查问题
        return Result.error(500, "服务器错误");
    }

    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<?> handleBusinessException(BusinessException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
        return Result.error(400, message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
        String message = e.getConstraintViolations().iterator().next().getMessage();
        return Result.error(400, message);
    }

     @ExceptionHandler(HttpMessageNotReadableException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public Result<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
         return Result.error(400, "请求参数格式错误");
     }
}
  • @RestControllerAdvice 注解表示这是一个全局异常处理器,可以处理所有 Controller 中抛出的异常。
  • @ExceptionHandler 注解用于指定要处理的异常类型。例如,@ExceptionHandler(Exception.class) 表示处理所有 Exception 类型的异常。
  • @ResponseStatus 注解用于指定 HTTP 状态码。例如,@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 表示返回 500 状态码。
  • 每个 @ExceptionHandler 方法都接收一个异常对象作为参数,可以从中获取异常信息。
  • BusinessException 是一个自定义的业务异常,我们稍后会定义它。
  • MethodArgumentNotValidExceptionConstraintViolationException 是 Bean Validation 框架抛出的异常,用于处理请求参数校验失败的情况。
  • HttpMessageNotReadableException 用于处理请求体无法解析的情况,例如 JSON 格式错误。

4. 自定义业务异常

为了更好地处理业务逻辑中的异常,我们可以定义一个自定义的业务异常类。

import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {

    private int code;
    private String message;

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

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
    }
}

这个 BusinessException 类继承了 RuntimeException,并添加了 codemessage 属性,用于表示业务异常的状态码和消息。

同时,我们还可以定义一个 ErrorCode 枚举类,用于管理所有业务异常的错误码。

import lombok.Getter;

@Getter
public enum ErrorCode {

    USER_NOT_FOUND(1001, "用户不存在"),
    PASSWORD_ERROR(1002, "密码错误"),
    ;

    private int code;
    private String message;

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

这样,我们就可以在业务代码中抛出自定义的业务异常,例如:

if (user == null) {
    throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}

5. 在 Controller 中使用

现在,我们可以在 Controller 中使用全局异常处理器和统一返回结构了。

import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public Result<User> getUser(@PathVariable Long id) {
        // 模拟根据 ID 获取用户
        User user = new User();
        user.setId(id);
        user.setName("张三");
        if (id == 0) {
            throw new BusinessException(ErrorCode.USER_NOT_FOUND);
        }
        return Result.success(user);
    }

    @PostMapping
    public Result<User> createUser(@Valid @RequestBody User user) {
        // 模拟创建用户
        return Result.success(user);
    }
}
  • @Valid 注解用于开启 Bean Validation 功能,对请求参数进行校验。
  • 如果 id 为 0,则抛出一个 BusinessException 异常。

6. Bean Validation 的使用

Bean Validation 是一种用于校验 Java Bean 的标准。Spring Boot 提供了对 Bean Validation 的支持,我们可以使用它来校验请求参数。

首先,需要在 pom.xml 文件中添加 Bean Validation 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

然后,在 User 类中添加校验注解:

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Data
public class User {

    private Long id;

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 10, message = "用户名长度必须在 2 到 10 个字符之间")
    private String name;

    @NotBlank(message = "密码不能为空")
    private String password;
}
  • @NotBlank 注解表示字段不能为空。
  • @Size 注解表示字段的长度必须在指定的范围内。

最后,在 Controller 中使用 @Valid 注解开启 Bean Validation 功能。

@PostMapping
public Result<User> createUser(@Valid @RequestBody User user) {
    // 模拟创建用户
    return Result.success(user);
}

如果请求参数校验失败,Spring Boot 会抛出一个 MethodArgumentNotValidException 异常,全局异常处理器会捕获这个异常,并将错误信息返回给客户端。

7. 全局异常处理的优先级问题

当存在多个 @ExceptionHandler 方法可以处理同一个异常时,Spring Boot 会根据以下规则选择合适的异常处理方法:

  1. 精确匹配: 如果存在一个 @ExceptionHandler 方法可以精确匹配异常类型,则选择该方法。
  2. 继承关系: 如果不存在精确匹配的 @ExceptionHandler 方法,则选择可以处理异常父类类型的 @ExceptionHandler 方法。
  3. 同级关系: 如果存在多个可以处理异常父类类型的 @ExceptionHandler 方法,则选择参数类型最接近异常类型的 @ExceptionHandler 方法。

例如,如果同时存在以下两个 @ExceptionHandler 方法:

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
    // ...
}

@ExceptionHandler(RuntimeException.class)
public Result<?> handleRuntimeException(RuntimeException e) {
    // ...
}

当抛出一个 NullPointerException 异常时,Spring Boot 会选择 handleRuntimeException 方法,因为它比 handleException 方法更接近 NullPointerException 类型。

8. 异步任务中的异常处理

在 Spring Boot 应用中,我们经常会使用异步任务来提高程序的性能。但是,异步任务中的异常处理也是一个需要注意的问题。由于异步任务是在独立的线程中执行的,所以全局异常处理器无法捕获异步任务中抛出的异常。

为了解决这个问题,我们可以使用 AsyncUncaughtExceptionHandler 接口来处理异步任务中的异常。

首先,需要创建一个类实现 AsyncUncaughtExceptionHandler 接口:

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;

import java.lang.reflect.Method;

public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        System.err.println("Exception message - " + ex.getMessage());
        System.err.println("Method name - " + method.getName());
        for (Object param : params) {
            System.err.println("Parameter value - " + param);
        }
        // 可以将异常信息记录到日志中,或者发送到监控系统
    }
}

然后,需要在配置类中配置 AsyncConfigurer

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

这样,当异步任务中抛出异常时,CustomAsyncExceptionHandler 就会被调用,我们可以将异常信息记录到日志中,或者发送到监控系统。

9. 总结与思考

我们学习了如何在 Spring Boot 中优雅地实现全局异常处理和统一返回结构。通过使用 @ControllerAdvice@ExceptionHandler 注解,我们可以将所有异常转化为统一的错误信息。通过定义一个通用的返回数据结构,我们可以让 API 的返回数据格式清晰一致。

现在,让我们思考一下,还有哪些方面可以改进?

  • 错误码管理: 我们可以使用数据库或者配置文件来管理错误码,方便维护和更新。
  • 国际化: 我们可以使用 Spring 的国际化功能,让错误信息支持多种语言。
  • 监控: 我们可以将异常信息发送到监控系统,方便统计和分析。

希望通过今天的学习,大家能够更好地理解和应用全局异常处理和统一返回结构,让我们的 API 更加健壮、易用和易维护。

优雅的异常处理和返回结构让代码更健壮

统一的返回结构让前后端分离更加彻底,全局异常处理避免代码冗余,清晰的错误码体系方便定位问题。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注