前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot - 统一格式封装及高阶全局异常处理

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

作者头像
小小工匠
发布2022-02-16 21:08:22
1.2K0
发布2022-02-16 21:08:22
举报
文章被收录于专栏:小工匠聊架构小工匠聊架构

文章目录

Pre

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

演进过程

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

代码语言:javascript
复制
 <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>

        <dependency>
            <groupId>jakarta.validationgroupId>
            <artifactId>jakarta.validation-apiartifactId>
        dependency>
 
    dependencies>

版本V1

代码语言:javascript
复制
@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 开发统一返回对象

代码语言:javascript
复制
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 
     * @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 
     * @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 
     * @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 
     * @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 
     * @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 
     * @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 约定接口状态码

代码语言:javascript
复制
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 验证

代码语言:javascript
复制
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

代码语言:javascript
复制
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方法的返回值,统一处理返回值/响应体, 加解密,签名等

代码语言:javascript
复制
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就正常写就可以了,不用统一格式去包装了,如下

代码语言:javascript
复制
@RestController
@RequestMapping("/v3")
public class ArtisanV3Controller {


    @GetMapping("/getString")
    public String getStr() {
        return "OOOOOOK";
    }


    @GetMapping("/getArtisan")
    public Artisan getArt() {
        Artisan artisan = new Artisan();
        artisan.setJob("CodeMonkey");
        artisan.setAge(18);
        return artisan;
    }

    @GetMapping("/getMockError")
    public int getMockError() {
        int i = 1 / 0;
        return i;
    }


}

测试下

看到问题了吧,当我们同时启用统一标准格式封装功能ResponseAdvice和RestExceptionHandler全局异常处理器时,统一格式增强功能会给返回的异常结果再次封装,所以跟前端的接口响应又迷糊了

Step2 全局异常整合到返回的标准格式

因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。

代码语言:javascript
复制
 // 若是统一返回类型,则不用再此封装
        if (o instanceof ResponseData) {
            return o;
        }

如果返回的结果是ResponseData对象,直接返回即可。

重新测试 ,

源码

https://github.com/yangshangwei/boot2

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-15 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • Pre
  • 演进过程
  • 版本V1
  • 版本2
    • Step1 约定统一返回格式
      • Step2 开发统一返回对象
        • Step3 约定接口状态码
          • Step4 验证
            • Step5 完善全局异常处理 @RestControllerAdvice + @ExceptionHandler
              • 全局异常处理器的必要行
              • 版本3 (ResponseBodyAdvice)
                • Step1 自定义ResponseBodyAdvice接口实现类
                  • Step2 全局异常整合到返回的标准格式
                  • 源码
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档