How does SpringBoot unify the back-end return format? Old birds all play like this!

飘渺Jam
中文

Hello everyone, I am Misty.

friendly returns to a unified standard format and how to handle global exceptions gracefully under the SpringBoot front-end and back-end separation development model.

First, let's take a look at why we need to return to a unified standard format?

Why return a unified standard format to SpringBoot

By default, there are three common SpringBoot return formats:

first type: Return String

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

The return value obtained by calling the interface at this time is as follows:

hello,javadaily

Second: return custom object

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

The return value obtained by calling the interface at this time is as follows:

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

third type: the interface is abnormal

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

The return value obtained by calling the interface at this time is as follows:

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

Based on the above situations, if you and the front-end developers jointly debug the interface, they will be very confused. Because we did not give him a unified format, the front-end personnel do not know how to deal with the return value.

What's more, some classmates such as Xiao Zhang like to encapsulate the result. He uses the Result object. Xiao Wang also likes to package the result, but he uses the Response object. When this happens, I believe the front end The staff will be crazy.

Therefore, we need to define a unified standard return format in our project.

Define the return standard format

A standard return format contains at least 3 parts:

  1. status status value: the backend uniformly defines the status codes of various returned results
  2. message description: description of the result of this interface call
  3. data: The data returned this time.
{
  "status":"100",
  "message":"操作成功",
  "data":"hello,javadaily"
}

Of course, you can also add other extended values as needed. For example, we added the interface call time to the returned object.

  1. timestamp: interface call time

Define the return object

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

}

Define status code

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

Unified return format

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

The return value obtained by calling the interface at this time is as follows:

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

This has indeed achieved the results we want. What I have seen in many projects is this way of writing. The ResultData.success() then returned to the front end.

Seeing this, we might as well stop and think, what are the disadvantages of doing this?

biggest drawback of 160f62021b0617 is that every time we write an interface, we need to call ResultData.success() this line of code to package the result, repetitive labor, waste of energy; and it is easy to be laughed at by other veterans.

image-20210716084136689

So we need to optimize the code. The goal is not to manually formulate the return value of ResultData

Advanced implementation

To optimize this code is very simple, we only need to use the ResponseBodyAdvice provided by SpringBoot.

The role of ResponseBodyAdvice: intercept the return value of the Controller method, uniformly process the return value/response body, generally used to unify the return format, encryption, decryption, signature, etc.

First look at the source code 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);
}

We only need to write a specific implementation class

/**
 * @author jam
 * @date 2021/7/8 10:10 上午
 */
@RestControllerAdvice
public 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);
    }
}

Need to pay attention to two places:

  • @RestControllerAdvice comment

    @RestControllerAdvice is an enhancement of the @RestController annotation, which can achieve three functions:

    1. Global exception handling
    2. Global data binding
    3. Global data preprocessing
  • String type judgment
if(o instanceof String){
  return objectMapper.writeValueAsString(ResultData.success(o));
} 

This code must be added. If the Controller returns String directly, SpringBoot returns directly, so we need to manually convert it to json.

After the above processing, we no longer need ResultData.success() , and directly return to the original data format, and SpringBoot automatically helps us realize the encapsulation of the packaging class.

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

At this time, the result of the data returned by the calling interface is:

{
  "status": 100,
  "message": "操作成功",
  "data": "hello,javadaily",
  "timestamp": 1626427373113
}

Does it feel perfect, don't worry, there is another question waiting for you.

image-20210716084552589

Interface abnormal problem

There is a problem at this time. Since we did not handle the exception of the Controller, once the method we call is abnormal, there will be a problem, such as the following interface

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

The result returned is:

image-20210708191106503

Obviously this is not the result we want. The interface reports an error and returns a successful response code. The front end will hit people after seeing it.

Don't worry, let's move on to the second topic, how to handle global exceptions gracefully.

Why does SpringBoot need a global exception handler

  1. No need to write try...catch by hand, unified capture by the global exception handler

    The biggest convenience of using the global exception handler is that programmers no longer need to write try...catch when writing code. As we said earlier, by default, the result returned by SpringBoot when an exception occurs is this:

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

try...catch to handle the exception

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

The goal we are pursuing is definitely not to manually write try...catch , but hope to be handled by the global exception handler.

  1. For custom exceptions, it can only be handled by the global exception handler
@GetMapping("error1")
public void empty(){
    throw  new RuntimeException("自定义异常");
}
  1. When we introduce the Validator parameter validator, an exception will be thrown if the parameter validation fails. At this time, it cannot be try...catch , and only the global exception handler can be used.

    SpringBoot integration parameter verification please refer to this article SpringBoot development cheats-integration parameter verification and advanced skills

How to implement a global exception handler

@Slf4j
@RestControllerAdvice
public 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());
    }

}

There are three details that need to be explained:

  1. @RestControllerAdvice , an enhanced class of RestController, which can be used to implement global exception handlers
  2. @ExceptionHandler , unified processing of a certain type of exception, thereby reducing code repetition rate and complexity, for example, to obtain custom exceptions can be @ExceptionHandler(BusinessException.class)
  3. @ResponseStatus specifies the http status code received by the client

Experience effect

At this time, we call the following interface:

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

The results returned are as follows:

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

Basically meet our needs.

But when we enable the unified standard format encapsulation functions ResponseAdvice and RestExceptionHandler global exception handlers at the same time, a new problem arises:

{
  "status": 100,
  "message": "操作成功",
  "data": {
    "status": 500,
    "message": "自定义异常",
    "data": null,
    "timestamp": 1625796167986
  },
  "timestamp": 1625796168008
}

The result returned at this time is like this, the unified format enhancement function will encapsulate the returned abnormal result again, so we need to solve this problem next.

Standard format returned by global exception access

It is very simple to make the global exception access to the standard format, because the global exception handler has already encapsulated the standard format for us, we only need to return it directly to the client.

@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));
  }
  if(o instanceof ResultData){
    return o;
  }
  return ResultData.success(o);
}

Key code:

if(o instanceof ResultData){
  return o;
}

If the returned result is a ResultData object, just return it directly.

At this time, we call the above error method again, and the returned result meets our requirements.

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

Okay, that's it for today's article. I hope that through this article you can master how to implement a unified standard format in your project friendly to return and handle global exceptions gracefully.

github address: https://github.com/jianzh5/cloud-blog/

Finally, I am Misty Jam, an architect who writes code, and a programmer who does architecture. I look forward to your attention. See you next time!

image.png

attention to the instructional video of 10 Gs, don’t you hurry to get in the car?

阅读 3.6k

JAVA杂谈
一个渣渣程序猿 [链接]

欢迎关注

332 声望
53 粉丝
0 条评论

欢迎关注

332 声望
53 粉丝
文章目录
宣传栏