大家好,我是飘渺。

明天咱们来聊一聊在基于SpringBoot前后端拆散开发模式下,如何敌对的返回对立的规范格局以及如何优雅的解决全局异样。

首先咱们来看看为什么要返回对立的规范格局?

为什么要对SpringBoot返回对立的规范格局

在默认状况下,SpringBoot的返回格局常见的有三种:

第一种:返回 String

@GetMapping("/hello")public String getStr(){  return "hello,javadaily";}

此时调用接口获取到的返回值是这样:

hello,javadaily

第二种:返回自定义对象

@GetMapping("/aniaml")public Aniaml getAniaml(){  Aniaml aniaml = new Aniaml(1,"pig");  return aniaml;}

此时调用接口获取到的返回值是这样:

{  "id": 1,  "name": "pig"}

第三种:接口异样

@GetMapping("/error")public int error(){    int i = 9/0;    return i;}

此时调用接口获取到的返回值是这样:

{  "timestamp": "2021-07-08T08:05:15.423+00:00",  "status": 500,  "error": "Internal Server Error",  "path": "/wrong"}

基于以上种种状况,如果你和前端开发人员联调接口她们就会很懵逼,因为咱们没有给他一个对立的格局,前端人员不晓得如何解决返回值。

还有甚者,有的同学比方小张喜爱对后果进行封装,他应用了Result对象,小王也喜爱对后果进行包装,然而他却应用的是Response对象,当呈现这种状况时我置信前端人员肯定会抓狂的。

所以咱们我的项目中是须要定义一个对立的规范返回格局的。

定义返回规范格局

一个规范的返回格局至多蕴含3局部:

  1. status 状态值:由后端对立定义各种返回后果的状态码
  2. message 形容:本次接口调用的后果形容
  3. data 数据:本次返回的数据。
{  "status":"100",  "message":"操作胜利",  "data":"hello,javadaily"}

当然也能够按需退出其余扩大值,比方咱们就在返回对象中增加了接口调用工夫

  1. timestamp: 接口调用工夫

定义返回对象

@Datapublic class ResultData<T> {  /** 后果状态 ,具体状态码参见ResultData.java*/  private int status;  private String message;  private T data;  private long timestamp ;  public ResultData (){    this.timestamp = System.currentTimeMillis();  }  public static <T> ResultData<T> success(T data) {    ResultData<T> resultData = new ResultData<>();    resultData.setStatus(ReturnCode.RC100.getCode());    resultData.setMessage(ReturnCode.RC100.getMessage());    resultData.setData(data);    return resultData;  }  public static <T> ResultData<T> fail(int code, String message) {    ResultData<T> resultData = new ResultData<>();    resultData.setStatus(code);    resultData.setMessage(message);    return resultData;  }}

定义状态码

public enum ReturnCode {    /**操作胜利**/    RC100(100,"操作胜利"),    /**操作失败**/    RC999(999,"操作失败"),    /**服务限流**/    RC200(200,"服务开启限流爱护,请稍后再试!"),    /**服务降级**/    RC201(201,"服务开启降级爱护,请稍后再试!"),    /**热点参数限流**/    RC202(202,"热点参数限流,请稍后再试!"),    /**零碎规定不满足**/    RC203(203,"零碎规定不满足要求,请稍后再试!"),    /**受权规定不通过**/    RC204(204,"受权规定不通过,请稍后再试!"),    /**access_denied**/    RC403(403,"无拜访权限,请分割管理员授予权限"),    /**access_denied**/    RC401(401,"匿名用户拜访无权限资源时的异样"),    /**服务异样**/    RC500(500,"零碎异样,请稍后重试"),    INVALID_TOKEN(2001,"拜访令牌不非法"),    ACCESS_DENIED(2003,"没有权限拜访该资源"),    CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),    USERNAME_OR_PASSWORD_ERROR(1002,"用户名或明码谬误"),    UNSUPPORTED_GRANT_TYPE(1003, "不反对的认证模式");    /**自定义状态码**/    private final int code;    /**自定义形容**/    private final String message;    ReturnCode(int code, String message){        this.code = code;        this.message = message;    }    public int getCode() {        return code;    }    public String getMessage() {        return message;    }}

对立返回格局

@GetMapping("/hello")public ResultData<String> getStr(){    return ResultData.success("hello,javadaily");}

此时调用接口获取到的返回值是这样:

{  "status": 100,  "message": "hello,javadaily",  "data": null,  "timestamp": 1625736481648,  "httpStatus": 0}

这样的确曾经实现了咱们想要的后果,我在很多我的项目中看到的都是这种写法,在Controller层通过 ResultData.success()对返回后果进行包装后返回给前端。

看到这里咱们无妨停下来想想,这样做有什么弊病呢?

最大的弊病就是咱们前面每写一个接口都须要调用 ResultData.success()这行代码对后果进行包装,重复劳动,节约膂力;而且还很容易被其余老鸟给讥笑。

所以呢咱们须要对代码进行优化,指标就是不要每个接口都手工制订 ResultData返回值。

高级实现形式

要优化这段代码很简略,咱们只须要借助SpringBoot提供的 ResponseBodyAdvice即可。

ResponseBodyAdvice的作用:拦挡Controller办法的返回值,对立解决返回值/响应体,个别用来对立返回格局,加解密,签名等等。

先来看下 ResponseBodyAdvice的源码:

public interface ResponseBodyAdvice<T> {        /**        * 是否反对advice性能        * true 反对,false 不反对        */    boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);      /**        * 对返回的数据进行解决        */    @Nullable    T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);}

咱们只须要编写一个具体实现类即可

/** * @author jam * @date 2021/7/8 10:10 上午 */@RestControllerAdvicepublic class ResponseAdvice 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) {        if(o instanceof String){            return objectMapper.writeValueAsString(ResultData.success(o));        }            return ResultData.success(o);    }}

须要留神两个中央:

  • @RestControllerAdvice注解

    @RestControllerAdvice@RestController注解的加强,能够实现三个方面的性能:

    1. 全局异样解决
    2. 全局数据绑定
    3. 全局数据预处理
  • String类型判断
if(o instanceof String){  return objectMapper.writeValueAsString(ResultData.success(o));} 

这段代码肯定要加,如果Controller间接返回String的话,SpringBoot是间接返回,故咱们须要手动转换成json。

通过下面的解决咱们就再也不须要通过 ResultData.success()来进行转换了,间接返回原始数据格式,SpringBoot主动帮咱们实现包装类的封装。

@GetMapping("/hello")public String getStr(){    return "hello,javadaily";}

此时咱们调用接口返回的数据后果为:

{  "status": 100,  "message": "操作胜利",  "data": "hello,javadaily",  "timestamp": 1626427373113}

是不是感觉很完满,别急,还有个问题在等着你呢。

接口异样问题

此时有个问题,因为咱们没对Controller的异样进行解决,当咱们调用的办法一旦出现异常,就会呈现问题,比方上面这个接口

@GetMapping("/wrong")public int error(){    int i = 9/0;    return i;}

返回的后果为:

这显然不是咱们想要的后果,接口都报错了还返回操作胜利的响应码,前端看了会打人的。

别急,接下来咱们进入第二个议题,如何优雅的解决全局异样。

SpringBoot为什么须要全局异样处理器

  1. 不必手写try...catch,由全局异样处理器对立捕捉

    应用全局异样处理器最大的便当就是程序员在写代码时不再须要手写 try...catch了,后面咱们讲过,默认状况下SpringBoot出现异常时返回的后果是这样:

{  "timestamp": "2021-07-08T08:05:15.423+00:00",  "status": 500,  "error": "Internal Server Error",  "path": "/wrong"}
这种数据格式返回给前端,前端是看不懂的,所以这时候咱们个别通过

try...catch来解决异样

@GetMapping("/wrong")public int error(){    int i;    try{        i = 9/0;    }catch (Exception e){        log.error("error:{}",e);        i = 0;    }    return i;}

咱们谋求的指标必定是不须要再手动写 try...catch了,而是心愿由全局异样处理器解决。

  1. 对于自定义异样,只能通过全局异样处理器来解决
@GetMapping("error1")public void empty(){    throw  new RuntimeException("自定义异样");}
  1. 当咱们引入Validator参数校验器的时候,参数校验不通过会抛出异样,此时是无奈用 try...catch捕捉的,只能应用全局异样处理器。

    SpringBoot集成参数校验请参考这篇文章SpringBoot开发秘籍 - 集成参数校验及高阶技巧

如何实现全局异样处理器

@Slf4j@RestControllerAdvicepublic class RestExceptionHandler {    /**     * 默认全局异样解决。     * @param e the e     * @return ResultData     */    @ExceptionHandler(Exception.class)    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)    public ResultData<String> exception(Exception e) {        log.error("全局异样信息 ex={}", e.getMessage(), e);        return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());    }}

有三个细节须要阐明一下:

  1. @RestControllerAdvice,RestController的加强类,可用于实现全局异样处理器
  2. @ExceptionHandler,对立解决某一类异样,从而缩小代码反复率和复杂度,比方要获取自定义异样能够@ExceptionHandler(BusinessException.class)
  3. @ResponseStatus指定客户端收到的http状态码

体验成果

这时候咱们调用如下接口:

@GetMapping("error1")public void empty(){    throw  new RuntimeException("自定义异样");}

返回的后果如下:

{  "status": 500,  "message": "自定义异样",  "data": null,  "timestamp": 1625795902556}

根本满足咱们的需要了。

然而当咱们同时启用统一标准格局封装性能 ResponseAdviceRestExceptionHandler全局异样处理器时又呈现了新的问题:

{  "status": 100,  "message": "操作胜利",  "data": {    "status": 500,    "message": "自定义异样",    "data": null,    "timestamp": 1625796167986  },  "timestamp": 1625796168008}

此时返回的后果是这样,对立格局加强性能会给返回的异样后果再次封装,所以接下来咱们须要解决这个问题。

全局异样接入返回的规范格局

要让全局异样接入规范格局很简略,因为全局异样处理器曾经帮咱们封装好了规范格局,咱们只须要间接返回给客户端即可。

@SneakyThrows@Overridepublic Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {  if(o instanceof String){    return objectMapper.writeValueAsString(ResultData.success(o));  }  if(o instanceof ResultData){    return o;  }  return ResultData.success(o);}

要害代码:

if(o instanceof ResultData){  return o;}

如果返回的后果是ResultData对象,间接返回即可。

这时候咱们再调用下面的错误方法,返回的后果就合乎咱们的要求了。

{  "status": 500,  "message": "自定义异样",  "data": null,  "timestamp": 1625796580778}

好了,明天的文章就到这里了,心愿通过这篇文章你能把握如何在你我的项目中敌对实现统一标准格局到返回并且能够优雅的解决全局异样。

github地址:https://github.com/jianzh5/cl...

最初,我是飘渺Jam,一名写代码的架构师,做架构的程序员,期待你的关注。咱们下期见!

关注即送10个G的教学视频,还不连忙上车?