SpringBoot practice: a move to achieve an elegant response to the results

premise

Regardless of the size of the system, most Spring Boot projects provide Restful + json interfaces for front-end or other service calls. The format is unified and standardized. It is not only a symbol that programs are kind to each other, but also a basic guarantee to reduce scolding during joint debugging.

Generally, the response result needs to include business status code, response description, response timestamp and response content, such as:

{
  "code": 200,
  "desc": "query was successful",
  "timestamp": "2020-08-12 14:37:11",
  "data": {
    "uid": "1597242780874",
    "name": "test 1"
  }
}

The service status code is divided into two factions: one is to recommend using HTTP response code as interface service return; The other is that all HTTP response codes return 200, and the response status is represented by a separate field in the response body. The two methods have their own advantages and disadvantages. I recommend the second one because many Web servers have the function of intercepting HTTP status codes, and the number of status codes is limited and not flexible enough. For example, returning 200 indicates that the interface processing is successful and the response is normal. Now a status code is required to indicate that the interface processing is successful and the response is normal, but the request data status is incorrect. You can return 2001.

Custom response body

Defining a data response body is the first step to return a unified response format. No matter whether the interface returns normally or an exception occurs, the structure format returned to the caller should remain unchanged. Give an example:

@ApiModel
@Data
public class Response<T> {
    @ApiModelProperty(value = "Return code", example = "200")
    private Integer code;
    @ApiModelProperty(value = "Return code description", example = "ok")
    private String desc;
    @ApiModelProperty(value = "Response timestamp", example = "2020-08-12 14:37:11")
    private Date timestamp = new Date();
    @ApiModelProperty(value = "Return results")
    private T data;
}

In this way, as long as the Response is returned in the Controller method, the interface Response is consistent, but many code templates with fixed format will be formed, such as the following:

@RequestMapping("hello1")
public Response<String> hello1() {
    final Response<String> response = new Response<>();
    response.setCode(200);
    response.setDesc("Return success");
    response.setData("Hello, World!");
    return response;
}

The response result of calling interface is:

{
  "code": 200,
  "desc": "Return success",
  "timestamp": "2020-08-12 14:37:11",
  "data": "Hello, World!"
}

How can this repetitive and non-technical code match the superior and elegant creature of the program ape? It's better to subtract those duplicate codes on the premise of returning the response results, such as:

@RequestMapping("hello2")
public String hello2() {
    return "Hello, World!";
}

This needs to be implemented with the help of the ResponseBodyAdvice provided by Spring.

Global processing of response data

First code:

/**
 * <br>created at 2020/8/12
 *
 * @author www.howardliu.cn
 * @since 1.0.0
 */
@RestControllerAdvice
public class ResultResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
        return !returnType.getGenericParameterType().equals(Response.class);// 1
    }

    @Override
    public Object beforeBodyWrite(final Object body, final MethodParameter returnType, final MediaType selectedContentType,
                                  final Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  final ServerHttpRequest request, final ServerHttpResponse response) {
        if (body == null || body instanceof Response) {
            return body;
        }
        final Response<Object> result = new Response<>();
        result.setCode(200);
        result.setDesc("query was successful");
        result.setData(body);
        if (returnType.getGenericParameterType().equals(String.class)) {// 2
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("take Response Object serialized as json Exception occurred while string", e);
            }
        }
        return result;
    }
}

/**
 * <br>created at 2020/8/12
 *
 * @author www.howardliu.cn
 * @since 1.0.0
 */
@RestController
public class HelloWorldController {
    @RequestMapping("hello2")
    public String hello2() {
        return "Hello, World!";
    }

    @RequestMapping("user1")
    public User user1() {
        User u = new User();
        u.setUid(System.currentTimeMillis() + "");
        u.setName("Test 1");
        return u;
    }
}

The above code is the template method of implementing the Spring ResponseBodyAdvice class. Just implement it according to the requirements of Spring. There are only two areas that need special attention, that is, where 1 and 2 are marked in the code.

First, say the line 1, that is, the supports method. This method is the pre judgment to verify whether the beforeBodyWrite method needs to be called. If it returns true, execute the beforeBodyWrite method. Here, judge whether beforeBodyWrite needs to be executed according to the return type of the Controller method, or return true all the time. Later, judge whether type conversion is required.

Then focus on the next line 2. This line is a pit, a big pit. If you are not familiar with the Spring structure, you will definitely linger here for a long time.

This line of code 2 is to judge whether the Controller method returns the result of String type. If so, serialize the returned object and return it.

This is because Spring handles the Response type of String type separately and uses the StringHttpMessageConverter class for data conversion. When processing the Response result, the size of the Response body will be calculated in the method getContentLength. The parent method definition is protected Long getContentLength(T t, @Nullable MediaType contentType), while the StringHttpMessageConverter rewrites the method to protected Long getContentLength(String str, @Nullable MediaType contentType). The first parameter is the Response object, Fixed write dead is of String type. If we force the return of Response object, ClassCastException will be reported.

Of course, there are few scenarios that directly return String. This pit may suddenly appear in a special interface one day.

Supplementary notes

The above only shows the simplest application of the ResponseBodyAdvice class. We can also implement more extensions. For example:

  1. Return request ID: this needs to be linked with RequestBodyAdvice. After obtaining the request ID, it will be placed in the response body when the response is received;

  2. Result data encryption: the response data is encrypted through ResponseBodyAdvice, which will not invade the business code, and the encryption level of the interface can be handled flexibly through annotation;

  3. Selective packaging response body: for example, define the annotation IgnoreResponseWrap on the interface that does not need packaging response body, and then judge the annotation of the method on the supports method, such as:

@Override
public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
    final IgnoreResponseWrap[] declaredAnnotationsByType = returnType.getExecutable().getDeclaredAnnotationsByType(IgnoreResponseWrap.class);
    return !(declaredAnnotationsByType.length > 0 || returnType.getGenericParameterType().equals(Response.class));
}

Many other ways of playing are not listed one by one.

summary

As mentioned above, the normal response data is only a little elegant. If you want to be complete, you also need to consider the interface exceptions. You can't wrap the business logic with a big try/catch/finally. That's too ugly. The next article will focus on how the interface can return a unified result response when an exception occurs.

Keywords: Java Back-end architecture RESTful

Added by Fabis94 on Thu, 04 Nov 2021 15:47:51 +0200