SpringBoot - 统一格式封装及高阶全局异常处理

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot - 统一格式封装及高阶全局异常处理相关的知识,希望对你有一定的参考价值。

文章目录


Pre

Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler实现自定义全局异常


演进过程

我们搞个boot工程 ,来看下为什么以及如何来实现统一格式封装及高阶全局异常处理

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>
 
    </dependencies>


版本V1


@RestController  // 返回JSON 
@RequestMapping("/v1")
public class ArtisanV1Controller 


    /**
     * 返回字符串
     *
     * @return
     */
    @GetMapping("/getString")
    public String getStr() 
        return "OOOOOOOK";
    

    /**
     * 返回自定义对象
     *
     * @return
     */
    @GetMapping("/getArtisan")
    public Artisan getArt() 
        Artisan artisan = new Artisan();
        artisan.setJob("ArtisanJob");
        artisan.setAge(18);
        return artisan;
    


    /**
     * 接口异常
     *
     * @return
     */
    @GetMapping("/getMockError")
    public int getMockError() 
        int i = 1 / 0;
        return i;
    



    

分别测试下

这混乱的格式, 前端同学怎么想


版本2

Step1 约定统一返回格式

一个合格的标准的返回格式至少包含3部分:

  • status 状态值

    由后端统一定义各种返回结果的状态码

  • message 描述

    本次接口调用的结果描述

  • data 数据

    本次接口返回的数据

如果需要可以加入其他节点,比如在返回对象中添加了接口调用时间 (timestamp: 接口调用时间)

Step2 开发统一返回对象

package com.artisan.resp;

import lombok.Data;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 公共结果 
 * @mark: show me the code , change the world
 */
@Data
public class ResponseData<T> 

    /**
     * 结果状态 ,具体状态码参见ResponseCode
     */
    private int status;


    /**
     * 响应消息
     **/
    private String message;

    /**
     * 响应数据
     **/
    private T data;

    /**
     * 接口请求时间
     **/
    private long timestamp;


    /**
     * 初始化,增加接口请求事件
     */
    public ResponseData() 
        this.timestamp = System.currentTimeMillis();
    


    /**
     * 成功
     *
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success() 
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC100.getCode());
        resultData.setMessage(ResponseCode.RC100.getMessage());
        return resultData;
    

    /**
     * 成功
     *
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success(String message) 
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC100.getCode());
        resultData.setMessage(message);
        return resultData;
    

    /**
     * 成功
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> success(T data) 
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC100.getCode());
        resultData.setMessage(ResponseCode.RC100.getMessage());
        resultData.setData(data);
        return resultData;
    

    /**
     * 失败
     *
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> fail(String message) 
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC999.getCode());
        resultData.setMessage(message);
        return resultData;
    

    /**
     * 失败
     *
     * @param code
     * @param message
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> fail(int code, String message) 
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(code);
        resultData.setMessage(message);
        return resultData;
    

    /**
     * 失败
     *
     * @param <T>
     * @return
     */
    public static <T> ResponseData<T> fail() 
        ResponseData<T> resultData = new ResponseData<>();
        resultData.setStatus(ResponseCode.RC999.getCode());
        resultData.setMessage(ResponseCode.RC999.getMessage());
        return resultData;
    



Step3 约定接口状态码

package com.artisan.resp;

import lombok.Getter;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 状态码集合
 * @mark: show me the code , change the world
 */
public enum ResponseCode 

    /**
     * 操作成功
     **/
    RC100(100, "操作成功"),

    /**
     * 操作失败
     **/
    RC999(999, "操作失败"),


    /**
     * access_denied
     **/
    RC403(403, "无访问权限,请联系管理员授予权限"),

    /**
     * access_denied
     **/
    RC401(401, "匿名用户访问无权限资源时的异常"),

    /**
     * 服务异常
     **/
    RC500(500, "系统异常,请稍后重试"),


    ILLEGAL_ARGUMENT(3001, "非法参数"),

    INVALID_TOKEN(2001, "访问令牌不合法"),

    ACCESS_DENIED(2003, "没有权限访问该资源"),

    CLIENT_AUTHENTICATION_FAILED(1001, "客户端认证失败"),

    USERNAME_OR_PASSWORD_NOTMATCH(1002, "用户名或密码错误"); 

    /**
     * 自定义状态码
     **/
    @Getter
    private final int code;
    /**
     * 自定义描述
     **/
    @Getter
    private final String message;

    ResponseCode(int code, String message) 
        this.code = code;
        this.message = message;
    



Step4 验证

package com.artisan.controller;

import com.artisan.entity.Artisan;
import com.artisan.resp.ResponseData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 版本2
 * @mark: show me the code , change the world
 */

@RestController
@RequestMapping("/v2")
public class ArtisanV2Controller 


    @GetMapping("/getString")
    public ResponseData<String> getStr() 
        return ResponseData.success("OOOOOOK");
    


    @GetMapping("/getArtisan")
    public ResponseData<Artisan> getArt() 
        Artisan artisan = new Artisan();
        artisan.setJob("CodeMonkey");
        artisan.setAge(18);
        return ResponseData.success(artisan);
    


    @GetMapping("/getMockError")
    public ResponseData<Integer> getMockError() 
        int i = 1 / 0;
        return ResponseData.success(i);
    



    

好像部分实现了统一格式返回,确实也是有很多项目在Controller层通过ResponseData.success()对返回结果进行包装后返回给前端。

但是这个抛异常的这么玩还是不行呀? ------------------------> 全局异常处理

Step5 完善全局异常处理 @RestControllerAdvice + @ExceptionHandler

package com.artisan.resp;

import com.artisan.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.stream.Collectors;


/**
 * @author 小工匠
 * @version 1.0
 * @description: 全局异常处理
 * @mark: show me the code , change the world
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler 

    /**
     * 默认全局异常处理。
     *
     * @param e e
     * @return ResponseData
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseData<String> exception(Exception e) 
        log.error("兜底异常信息 ex=", e.getMessage());
        return ResponseData.fail(ResponseCode.RC500.getCode(), e.getMessage());
    

    /**
     * Assert异常
     */
    @ExceptionHandler(IllegalArgumentException.class, IllegalStateException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseData<String> exception(IllegalArgumentException e) 
        return ResponseData.fail(ResponseCode.ILLEGAL_ARGUMENT.getCode(), e.getMessage());
    


    /**
     * 抓取自定义异常  BaseException
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseData<String> exception(BaseException e) 
        return ResponseData.fail(e.getErrorCode(), e.getMessage());
    

 


  • @ExceptionHandler,统一处理某一类异常, 减少代码重复率和复杂度,比如要捕获自定义异常可以@ExceptionHandler(BusinessException.class)

  • @ResponseStatus指定客户端收到的http状态码

重新验证下


全局异常处理器的必要行

  1. 避免try…catch,由全局异常处理器统一捕获
  2. 自定义异常,只能通过全局异常处理器来处理
  3. Validator参数校验器的时候,参数校验不通过会抛出异常,无法用try…catch捕获,只能使用全局异常处理器。


版本3 (ResponseBodyAdvice)

V2版本有缺陷么?

我们不难发现每写一个接口都需要调用ResponseData.success()对结果进行包装 ,程序猿懒啊, 能不写吗


Step1 自定义ResponseBodyAdvice接口实现类

ResponseBodyAdvice的作用一般是用于拦截Controller方法的返回值,统一处理返回值/响应体, 加解密,签名等

package com.artisan.resp.v3;

import com.artisan.resp.ResponseData;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @author 小工匠
 * @version 1.0
 * @description: 拦截Controller方法的返回值,统一处理返回值/响应体
 * @mark: show me the code , change the world
 */

@RestControllerAdvice
public class CustomResponseAdvice implements ResponseBodyAdvice<Object> 

    @Autowired
    private ObjectMapper objectMapper;


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) 
        return true;
    

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) 
        // 处理String类型 
        if (o instanceof String) 
            return objectMapper.writeValueAsString(ResponseData.success(o));
        
        // 若是统一返回类型,则不用再此封装
//        if (o instanceof ResponseData) 
//            return o;
//        
        return ResponseData.success(o);
    

    
  • @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器

  • 有一个地方对String做了特殊处理,因为如果Controller直接返回String ,SpringBoot是直接返回,所以我们需要手动转换成json


接入@RestControllerAdvice后, Controller就正常写就可以了,不用统一格式去包装了,如下

@RestCont

以上是关于SpringBoot - 统一格式封装及高阶全局异常处理的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot全局异常统一处理反参标准化

SpringBoot全局异常统一处理反参标准化

SpringBoot返回统一的JSON标准格式

SpringBoot返回统一的JSON标准格式

SpringBoot RESTful API返回统一数据格式还不懂?

每天用SpringBoot,还不懂RESTful API返回统一数据格式是怎么实现的?