Springboot项目中全局异常处理的优雅实现



文章目录

  • Springboot项目中全局异常处理的优雅实现
  • 背景
  • 一、核心思路
  • 二、实现示例
  • 1.反参协议
  • 2.异常定义
  • 3.全局异常的捕获与处理
  • 4.API调用的统一反参协议承装
  • 总结



背景

在前后端分离式开发场景下,前端往往需要得到一个标准化的、通用的json反参格式,反参格式的通用性不仅要覆盖API正常调用,还要满足触发的业务异常(如账号已存在、用户数量超限等需要返回给前端的具体业务异常)与系统异常(栈溢出、数组越界、空指针等需要统一封装后返回前端的系统异常)。
这时便需要我们对业务中的特定异常与系统异常进行全局捕获并统一处理了,本文将重点阐述如何优雅的实现Springboot项目中全局异常的处理。


一、核心思路

作者认为,实现全局异常捕捉的核心有如下几点:

  1. 一个统一的、具备高通用性的反参协议。
  2. 业务异常与系统异常的处理形式定义。
  3. API调用成功与否,操作成功或失败(由业务逻辑决定)的返回形式。

下文将提供一个简单示例以供参考。

二、实现示例

1.反参协议

一个通用性较高的反参协议可以包括:

  1. 状态码,以便前端可根据不同状态码提供不同的响应机制。
  2. 消息,与状态码对应的消息描述。
  3. API调用是否成功,操作是否成功。
  4. 后端返回的数据。
package com.example.demo.httpResult;

import lombok.Data;

/**
 * @author Overwatch
 */
@Data
public class HttpResult<T> {
    private String code;
    private String message;
    private T data;

    /**
     * 默认为调用成功
     */
    private Boolean success = true;

    public HttpResult(){

    }

    public HttpResult(String code, String message, T data){
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public HttpResult(String code, String message, T data, Boolean success){
        this.code = code;
        this.message = message;
        this.data = data;
        this.success = success;
    }

    public HttpResult(T data) {
        this.data = data;
        this.code = HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getCode();
        this.message = HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getMessage();
    }

    public HttpResult(String message){
        this.data = null;
        this.message = message;
        this.code = HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getCode();
        this.success = true;
    }
}

HttpBaseResponseEnum是作者封装的一个返回接口调用成功或失败的默认值枚举类,代码如下:

package com.example.demo.httpResult;

/**
 * @author Overwatch
 */
public enum HttpBaseResponseEnum {

    HTTP_RESPONSE_SUCCESS("200", "ok"),

    HTTP_RESPONSE_FAIL("-1", "系统错误"),
    ;

    private String code;
    private String message;

    public 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;
    }

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

2.异常定义

异常信息的核心组成是code和message,首先定义一个base interface用于全局异常的捕捉:

package com.example.demo.exception;

/**
 * 全局异常捕获interface
 * @author Overwatch
 */
public interface ExceptionInfo {

    /**
     * 获取异常码
     * @return
     */
    String getCode();

    /**
     * 获取异常信息
     * @return
     */
    String getMessage();
}

其次,可以将异常分类,一般来说可分为业务异常与系统异常,这里以业务异常进行代码举例:

package com.example.demo.exception;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Overwatch
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BusinessException extends RuntimeException{
    private String code;
    private String message;

    public BusinessException(ExceptionInfo exceptionInfo) {
        this.code = exceptionInfo.getCode();
        this.message = exceptionInfo.getMessage();
    }

}

接下来可以细化业务异常,定义具体的内容,并实现上文定义的异常接口,这里采用枚举类作为示例:

package com.example.demo.exception;


/**
 * @author Overwatch
 */

public enum RoleExceptionEnum implements ExceptionInfo {
    /**
     * 角色已存在
     */
    ROLE_IS_EXIST("200000", "角色名已存在"),
    ;

    private String code;

    private String message;

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

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

3.全局异常的捕获与处理

接下来是本文的重点内容,全局异常的捕获与处理,其核心是@RestControllerAdvice与@ExceptionHandler注解的应用。这里也可以使用@ControllerAdvice来替代@RestControllerAdvice,但是往往反参是json格式的数据,因此还要在方法上标注@ResponseBody注解,换句话说,这里可以理解为@RestControllerAdvice = @ControllerAdvice + @ResponseBody。代码示例如下:

package com.example.demo.advice;

import com.example.demo.exception.BusinessException;
import com.example.demo.httpResult.BaseController;
import com.example.demo.httpResult.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @author Overwatch
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(value = BusinessException.class)
    public HttpResult<Object> businessExceptionHandler(BusinessException e){
        log.error("发生业务异常", e);

        return BaseController.httpResponseFail(e);
    }
}

如代码所示,通过将业务异常实体类作为@ExceptionHandler的传值,以实现对业务异常的全局捕获,并在捕获方法中自定义处理业务异常的逻辑。BaseController是作者自定义的一个API调用的一个统一反参协议载体,将在后文详细阐述。

4.API调用的统一反参协议承装

万事俱备,只欠东风。现在已经有了统一的反参协议,自定义异常,自定义异常的捕获与处理,还差一个载体,就完美了。
载体的作用便是提供在API调用成功或失败、业务操作是否成功下的反参协议承装。代码如下所示:

package com.example.demo.httpResult;

import com.example.demo.exception.BusinessException;

/**
 * @author Overwatch
 */
public class BaseController {

    public static HttpResult httpResponseOk(Object data){
        return new HttpResult(HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getCode(),
                HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getMessage(), data);
    }

    public static HttpResult httpResponseOk(){
        return new HttpResult(HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getCode(),
                HttpBaseResponseEnum.HTTP_RESPONSE_SUCCESS.getMessage(), null, true);
    }

    public static HttpResult<Object> httpResponseFail(BusinessException e){
        return new HttpResult(e.getCode(), e.getMessage(), null, false);
    }

    public static HttpResult<Object> httpResponseFail(){
        return new HttpResult(HttpBaseResponseEnum.HTTP_RESPONSE_FAIL.getCode(),
                HttpBaseResponseEnum.HTTP_RESPONSE_FAIL.getMessage(), null, false);
    }

    public static HttpResult<Object> httpResponseFail(String code, String message){
        return new HttpResult(code, message, null, false);
    }

}

基于此反参协议载体下的接口定义示例如下:

package com.example.demo.api.controller;

import com.example.demo.api.controller.vo.RoleVO;
import com.example.demo.api.param.RoleParam;
import com.example.demo.httpResult.BaseController;
import com.example.demo.httpResult.HttpResult;
import com.example.demo.service.RoleService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * @author Overwatch
 */
@RestController
@RequestMapping("/role")
public class RoleController extends BaseController {
    @Autowired
    private RoleService roleService;

    @PostMapping
    @ApiOperation("创建角色")
    public HttpResult<String> addRole(@RequestBody @Validated RoleParam param){
        roleService.addRole(param);
        return httpResponseOk();
    }

    @GetMapping("/actions/getByRoleName")
    @ApiOperation("根据角色名获取角色信息")
    public HttpResult<RoleVO> getRoleByName(@RequestParam("roleName") String roleName){
        return httpResponseOk(roleService.getRoleByName(roleName));
    }
}

作者以addRole方法举例,在该方法中提供一个校验角色名是否重复的示例,角色名若重复,则会抛出相应的业务异常:

Assert.isFalse(roleNameIsExist(param.getRoleName()), RoleExceptionEnum.ROLE_IS_EXIST);

下面是接口调用的反参结果示例:

操作成功:

spring boot 设置全局表前缀 springboot全局对象_HTTP


操作失败:

spring boot 设置全局表前缀 springboot全局对象_spring boot_02


总结

本文以业务异常为例,阐述了Springboot的全局异常处理的实现方式,该方式同样适用于系统异常。