Spring Boot全局异常捕获与统一返回类型

3,482 阅读5分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

在我们的项目中,对异常的捕捉是十分重要的。所以我们通常会在业务代码中写一堆的try...catch去捕捉异常。但是这样做,十分影响我们代码的可读性,并且也十分的繁琐。我们需要一种统一的方法去帮我们处理这些异常。

知识点

参考:

Spring Framework Documentation

Exception Handling in Spring MVC

@ResponseStatus

很多情况下,在处理 Web 请求时抛出的任何未处理的异常都会导致服务器返回 HTTP 500 响应。我们在编写自定义异常时,可以指定HTTP状态码

@ExceptionHandler - 基于控制器的异常处理

可以在控制器中添加含有@ExceptionHandler注解的方法,这样可以处理在同一控制器中引发的异常。

  • 处理没有@ResponseStatus注解的异常
  • 将用户重定向到专用的错误视图
  • 建立完全自定义的错误相应

@ControllerAdvice - 全局异常处理

含有@ControllerAdvice注解的类允许异常处理应用于整个应用程序,而不是单个的控制器中,这个类支持以下三种类型的方法。

  • @ExceptionHandler注解的方法 - 全局异常处理
  • @ModelAttribute注解的模型增强方法(向模型中添加额外的数据)- 全局数据绑定
  • @InitBinder注解的Binder初始化方法(用于表单配置)- 全局数据预处理

@ControllerAdvice包含@Component注解,这意味着用该注解的类会被注册成Spring Bean

@RestControllerAdvice

@RestControllerAdvice包含@ControllerAdvice@ResponseBody

这意味着返回值是Web响应的主体(body)。一般用于返回JSON格式的数据。

在启动时,@RequestMapping@ExceptionHanlder的基础类方法会检测带有@ControllerAdvice的Bean,并在运行时应用他们全局的@ExceptionHandler方法(来自@ControllerAdvice),全局的方法会在本地方法(来自@Controller)之后应用。相比之下,全局的@ModelAttribute@InitBinder方法会在本地方法之前应用。

默认情况下,@ControllerAdvice方法适用于所有的Controller,但也可以通过注解的属性将其缩小到Controller的子集

// 对@RestController注解的类生效
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 对某个特定的包下的controller生效
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 对特定的类生效
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

统一返回类型

参考:

SpringBoot构建电商基础秒杀项目

现在的许多项目都是采用前后端分离,因此约定相同的返回类型有助于高效地工作。

通常的返回类型包含以下三个信息

  • code - 状态码
  • message - 状态码对应的简单描述
  • data - 数据

当我们能成功获取数据时,状态码起的作用不大,一般只表示成功获取了请求的信息。但当获取数据异常时,我们能根据状态码去告知用户发生了什么事,应该怎么做。

我们封装一个接口,这个接口包含了一些关于状态码的操作。

/**
 * 业务状态码封装接口
 */
public interface CommonCode {
    int getCode();
    String getMsg();
    void setMsg(String msg);
}

创建一个枚举类实现该接口,并在此定义一些状态码。

public enum BusinessCodeEnum implements CommonCode {

    SUCCESS(20000,"SUCCESS"),

    ERROR(50000,"未知错误"),
    NULL_POINTER_ERROR(50001,"空指针错误"),

    PARAMETER_VALIDATION_ERROR(10001, "参数不合法"),

    ERROR_USERNAME_OR_PASSWORD(20001, "用户名或密码错误"),
    ;

    private Integer code;
    private String msg;

    BusinessCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }


    @Override
    public int getCode() {
        return this.code;
    }

    @Override
    public String getMsg() {
        return this.msg;
    }

    @Override
    public void setMsg(String msg) {
        this.msg = msg;
    }

}

为了能更方便地处理业务异常,我们创建一个业务异常类,它继承了Exception类并且实现了CommonCode接口。我们可以直接通过刚刚定义的状态码去创建业务异常。

import lombok.NoArgsConstructor;

@NoArgsConstructor
public class BusinessException extends Exception implements CommonCode {

    private CommonCode commonCode;

    /**
     * 接受BusinessCodeEnum的传参用于构造业务异常
     * @param commonCode
     */
    public BusinessException(CommonCode commonCode){
        super();
        this.commonCode = commonCode;
    }

    /**
     * 接收自定义msg的方式构造业务异常
     * @param commonCode
     * @param msg
     */
    public BusinessException(CommonCode commonCode, String msg){
        super();
        this.commonCode = commonCode;
        this.commonCode.setMsg(msg);
    }

    @Override
    public int getCode() {
        return this.commonCode.getCode();
    }

    @Override
    public String getMsg() {
        return this.commonCode.getMsg();
    }

    @Override
    public void setMsg(String msg) {
        this.commonCode.setMsg(msg);
    }
}

创建统一返回的结果类

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class CommonReturnType<T> {

    /**
     * 状态码
     */
    private Integer code;

    /**
     * 结果信息
     */
    private String message;

    /**
     * 实体对象数据
     */
    private T data;

    /**
     * 通过BusinessCodeEnum实例化结果类
     * @param businessCodeEnum
     */
    private CommonReturnType(BusinessCodeEnum businessCodeEnum){
        this.setCode(businessCodeEnum);
    }

    /**
     * 通过code和message实例化结果类
     * @param code      状态码
     * @param message   结果信息
     */
    private CommonReturnType(Integer code, String message){
        this.code = code;
        this.message = message;
    }

    /**
     * 成功 - 返回实体类结果对象
     * @param data 实体类
     * @param <T> 实体类的类型
     * @return 带有实体类的结果对象
     */
    public static <T> CommonReturnType<T> success(T data){
        CommonReturnType<T> returnType = new CommonReturnType<>();
        returnType.setCode(BusinessCodeEnum.SUCCESS);
        returnType.setData(data);
        return returnType;
    }


    /**
     * 错误 - 通过业务错误码直接返回结果
     * @param businessCodeEnum 状态码枚举类
     * @return 只含状态码信息的结果对象
     */
    public static CommonReturnType error(BusinessCodeEnum businessCodeEnum){
        return new CommonReturnType(businessCodeEnum);
    }

    /**
     * 通过code和message直接返回结果类
     * @param code      状态码
     * @param message   结果信息
     * @return 只含状态码信息的结果对象
     */
    public static CommonReturnType error(Integer code, String message){
        return new CommonReturnType(code, message);
    }

    /**
     * 通过BusinessCodeEnum设置code和message
     * @param businessCodeEnum 状态码枚举类
     */
    public void setCode(BusinessCodeEnum businessCodeEnum){
        this.code = businessCodeEnum.getCode();
        this.message = businessCodeEnum.getMsg();
    }
}

在这个统一的结果类中,我们封装了几种会经常用到的方法。

  • 当我们成功拿到数据时,状态码无需特别设置,我们只用填入需要获取的数据即可
  • 当我们在捕捉到异常时,需要根据抛出的异常去返回错误信息,只需填入定义好的错误状态码
  • 当我们捕捉到业务异常时,根据异常中含有的状态码和信息构造结果类返回

全局异常配置

通过前面的配置,到了这里就很简单了。创建一个全局异常处理类。

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = BusinessException.class)
    public CommonReturnType handleBusinessException(BusinessException e){
        return CommonReturnType.error(e.getCode(),e.getMsg());
    }

    @ExceptionHandler(value = NullPointerException.class)
    public CommonReturnType handleNullPointerException(){
        return CommonReturnType.error(BusinessCodeEnum.NULL_POINTER_ERROR);
    }

    @ExceptionHandler(value = Exception.class)
    public CommonReturnType handleException(){
        return CommonReturnType.error(BusinessCodeEnum.ERROR);
    }
}

直接抛出异常即可,例如。

public UserDO findById(Integer id) throws BusinessException {
	UserDO userDO = userDao.findById(id);
	if(userDO == null){
		throw new BusinessException(BusinessCodeEnum.ERROR_USERNAME_OR_PASSWORD);
	}
	return userDO;
}

总结

本文主要讲述了如何创建统一的返回类型以及在Spring Boot中的全局异常处理