Interviewer: how to play with SpringBoot unified interface return and global exception handling?

Posted by wsantos on Sun, 02 Jan 2022 06:01:26 +0100

At present, most company project frameworks basically belong to the front-end and back-end separation mode. This mode will involve a front-end and back-end docking problem. It is very necessary to maintain a complete and standardized interface for front-end or back-end services, which can not only improve the docking efficiency, but also make my code look more concise and elegant.

The biggest difference between before and after modification is that we do not need to capture exceptions separately on each interface, nor do we need to assemble return parameters on each interface. Please refer to the following comparison diagram:

1, SpringBoot does not use a uniform return format

By default, SpringBoot will have the following three return situations.

1.1 string

@GetMapping("/getUserName")
public String getUserName(){
    return "HuaGe";
}

Return result of calling interface:

HuaGe

1.2 entity class

@GetMapping("/getUserName")
public User getUserName(){
    return new User("HuaGe",18,"male");
}

Return result of calling interface:

{
  "name": "HuaGe",
  "age": "18",
  "Gender": "male", 
}

1.3 exception return

@GetMapping("/getUserName")
public static String getUserName(){
    HashMap hashMap = Maps.newHashMap();
    return hashMap.get(0).toString();
}

Simulate a null pointer exception. Without any exception handling, you can see the default return result of SpringBoot:

{
    "timestamp": "2021-08-09T06:56:41.524+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/sysUser/getUserName"
}

For the above cases, if the whole project does not define a unified return format, five background developers define five return formats, which not only leads to bloated code and low efficiency of front-end and back-end docking, but also some unexpected situations, such as the front-end direct display of exception details, which gives a very poor user experience.

2, Basic play

The most common thing in the project is to encapsulate a tool class, which defines the field information to be returned, and encapsulates the interface information to be returned to the front end through this class, so as to solve the phenomenon of inconsistent return format.

2.1 parameter description

  • Code: status code. A unified set of status codes can be maintained in the background;
  • message: description information, prompt information of success / failure of interface call;
  • Data: return data.

2.2 process description

  • New Result class
public class Result<T> {
    
    private int code;
    
    private String message;
    
    private T data;

    public Result() {}
    public Result(int code, String message) {
        this.code = code;
        this.message = message;
    }
    
    /**
     * success
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<T>();
        result.setCode(ResultMsgEnum.SUCCESS.getCode());
        result.setMessage(ResultMsgEnum.SUCCESS.getMessage());
        result.setData(data);
        return result;
    }

    /**
     * fail
     */
    public static <T> Result<T> error(int code, String message) {
        return new Result(code, message);
    }
}
  • Define return status code
public enum ResultMsgEnum {
    SUCCESS(0, "success"),
    FAIL(-1, "fail"),
    AUTH_ERROR(502, "privilege grant failed!"),
    SERVER_BUSY(503, "The server is busy. Please try again later!"),
    DATABASE_OPERATION_FAILED(504, "Database operation failed");
    private int code;
    private String message;
​
    ResultMsgEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }
    public int getCode() {
        return this.code;
    }
    
    public String getMessage() {
        return this.message;
    }
}
  • Mode of use

The above two steps define the data return format and status code. Next, we'll see how to use it in the interface.

@GetMapping("/getUserName")
public Result getUserName(){
    return Result.success("huage");
}

The call Result is as follows. You can see that it is the parameter type defined in Result.

{
    "code": 0,
    "message": "success",
    "data": "huage"
}

Although this can meet daily needs, and I believe many small partners use it, if we have a large number of interfaces and use result in each interface Success to package the returned information will add a lot of duplicate codes, which is not elegant enough, and even embarrassed to show off. There must be a way to improve the code lattice again and achieve the optimal solution.

3, Advanced usage

After learning the basic usage, we will focus on the research version, which mainly uses the following two knowledge points. The usage is simple. It is a necessary skill for both teaching younger sister and instructing younger sister.

3.1 class introduction

3.1 class introduction

3.1 class introduction

  • ResponseBodyAdvice: this interface is provided by spring MVC 4.1. It allows you to customize the returned data after executing @ ResponseBody, which is used to encapsulate the returned data in a unified data format;
  • @RestControllerAdvice: this annotation enhances the Controller and can catch the thrown exceptions globally.

3.2 instructions

  • Create a new ResponseAdvice class;
  • Implement the ResponseBodyAdvice interface and the supports and beforeBodyWrite methods;
  • This class is used to uniformly encapsulate the returned results of the interface in the controller.
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
​
    /**
     * Enable function: true: Yes 
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
​
    /**
     * Processing returned results
     */
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //Processing string type data
        if(o instanceof String){
            try {
                return objectMapper.writeValueAsString(Result.success(o));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return Result.success(o);
    }
}
​

We can test the getUserName interface and find that it is consistent with the Result returned directly by using Result.

However, careful partners must have noticed that we all use result in response advice Success (o) is used to process the results, and the results of error type are not processed. Let's take a look at the return result when an exception occurs? Continue to use the above code of HashMap null pointer exception, and the test results are as follows:

{
    "code": 0,
    "message": "success",
    "data": {
        "timestamp": "2021-08-09T09:33:26.805+00:00",
        "status": 405,
        "error": "Method Not Allowed",
        "path": "/sysUser/getUserName"
    }
}

Although there is nothing wrong with the format, it is unfriendly or incorrect in the specific data of code and data fields. If you don't handle these things well, you will seriously affect your tall image in the front-end sister's heart, which can never be tolerated.

3.3 global exception handler

In the past, when we encountered an exception, the first thing we thought of was try catch.. Final, but this method will lead to a lot of code duplication, difficult maintenance, bloated logic and other problems, which is not the result we want.

The global exception handling method we are going to use today is relatively simple to use. First, add a new class and add the @ RestControllerAdvice annotation. The role of this annotation has been described above, so I won't nag anymore.

@RestControllerAdvice
public class CustomerExceptionHandler {
    
}

If we have an exception type that we want to intercept, we will add a new method and modify it with @ ExceptionHandler annotation. The annotation parameter is the target exception type.

For example, when an Exception occurs on the interface in the controller, it will enter the Exception method for capture, convert the messy Exception information into the specified format, give it to the ResponseAdvice method for unified format encapsulation, and return it to the front-end partner.

@RestControllerAdvice
@Slf4j
public class CustomerExceptionHandler {
​
    @ExceptionHandler(AuthException.class)
    public String ErrorHandler(AuthorizationException e) {
        log.error("Permission verification failed!", e);
        return "Permission verification failed!";
    }
​
    @ExceptionHandler(Exception.class)
    public Result Execption(Exception e) {
        log.error("Unknown exception!", e);
        return Result.error(ResultMsgEnum.SERVER_BUSY.getCode(),ResultMsgEnum.SERVER_BUSY.getMessage());
    }
}

When you call interface getUserName again to check the returned results, you will find that there are still some problems, because we have encapsulated the returned results of the interface into Result type in CustomerExceptionHandler, and the results will be encapsulated again when the code is executed to the unified Result return class ResponseAdvice. The following problems occur.

{
    "code": 0,
    "message": "success",
    "data": {
        "code": 503,
        "message": "The server is busy. Please try again later!",
        "data": null
    }
}

3.4 unified return result processing class final version

Solving the above problem is very simple. Just add a judgment in beforeBodyWrite.

@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
​
    /**
     * Enable function true: Enable
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
​
    /**
     * Processing returned results
     */
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //Processing string type data
        if(o instanceof String){
            try {
                return objectMapper.writeValueAsString(Result.success(o));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        //Is the return type encapsulated
        if(o instanceof Result){
            return o;
        }
        return Result.success(o);
    }
}
​

So far, all the tasks in this chapter are finished. The above code can be directly referenced without other configuration items. It is highly recommended to reference it to your own project.

4, Summary

This chapter does not explain much. It mainly focuses on the configuration of two classes, namely, the class ResponseAdvice for handling unified returned results and the exception handling class CustomerExceptionHandler. The full text focuses on the use of RestControllerAdvice and ResponseBodyAdvice, and finally constructs a set of general code return format through step-by-step iteration. This article belongs to practical knowledge sharing. I don't know that there are other easy-to-use unified processing schemes for my partners. If there are better schemes, we can share them and learn together.

Topics: Java JavaEE Back-end