There are a bunch of services where existing interfaces return entity objects directly, such as:

{
    "name": "Zhang"."city": "Shanghai"
}
Copy the code

All interfaces are now required to support uniform packaging and to be compatible with previous versions. To do this, we have a convention to return the new wrapper as long as the interface has an app-Adapter =open header, and the previous wrapper if not. The packaging format is as follows:

{
    "code": 0."message": "ok"."timestamp": 1627384576680."data": {
        "name": "Zhang"."city": "Shanghai"}}Copy the code

Traditional solutions

Spirng provides a ResponseBodyAdvice interface that processes the response before the message converter performs the conversion. This is easily supported in conjunction with the @RestControllerAdvice annotation. The following is a code example:

@RestControllerAdvice
public class AppResponseBodyAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        final String header = request.getHeader(AdapterRestResponseMessageProvider.HEADER);
        // If the type returned by the interface itself is ResultVO, there is no need to perform additional operations. Return false
        returnObjects.equals(header, AdapterRestResponseMessageProvider.HEADER_VALUE) && ! returnType.getGenericParameterType().equals(Result.class); }@Override
    @SneakyThrows
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // The String type cannot be wrapped directly, so it needs special treatment
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(Result.ok(body));
        }
        // Wrap the original data in a ResultVO
        return Result.ok(body);
    }
    
    @Data
    public static class Result<T> {

        public static final int OK = 0;

        /** * Returns code 200 as normal */
        private int code;

        /** * messages */
        private String message;

        /** * return timestamp */
        private long timestamp;

        /** * Return data */
        private T data;


        public static <T> Result<T> ok(T data) {
            Result<T> re = new Result<T>();
            re.code = OK;
            re.message = "ok";
            re.data = data;
            re.timestamp = System.currentTimeMillis();
            returnre; }}}Copy the code

This approach is simple enough, but it also has the obvious drawback of having to copy the code in each service. As an aspiring programmer, this kind of implementation must be intolerable. A generic solution must be found that all services can be supported by simply introducing dependencies.

Analysis of the

The idea is definitely to use the ResponseBodyAdvice interface, but the key is to weave it into Spring logic.

Let’s take a look at how the ResponseBodyAdvice interface actually works from a source code perspective. First, you can determine that the transformation logic must be executed where the value returned by the method is processed. Direct viewing ServletInvocableHandlerMethod. InvokeAndHandle () :

【Spring source code reading 】MVC implementation principle

Continue to follow up HandlerMethodReturnValueHandlerComposite. HandleReturnValue ()

Continue to follow up RequestResponseBodyMethodProcessor. HandleReturnValue ()

The realization of the writeWithMessageConverters () method in the superclass AbstractMessageConverterMethodProcessor.

As you can see, ResponseBodyAdvice executes at the location of the red box in the figure. Next, we need to find out how the Advice is injected and figure out how to manually inject our implementation as well.

Advice is actually a superclass AbstractMessageConverterMethodArgumentResolver fields, and in the constructor for the instantiation.

private final RequestResponseBodyAdviceChain advice;
Copy the code

The construction method of the structure of the caller is AbstractMessageConverterMethodProcessor method, level is RequestResponseBodyMethodProcessor up again.

Continue to analyze where RequestResponseBodyMethodProcessor is instantiated, according to the call, Can determine is in our familiar RequestMappingHandlerAdapter getDefaultReturnValueHandlers () method. This method is called inside afterPropertiesSet().

Here is requestResponseBodyAdvice RequestMappingHandlerAdapter an attribute.

private List<Object> requestResponseBodyAdvice = new ArrayList<>();
Copy the code

Analysis here, we can conclude, ResponseBodyAdvice is initialized in RequestMappingHandlerAdapter instance when injected.

The solution

Through the above analysis, we have to do things are very obvious, that is after RequestMappingHandlerAdapter instantiation, initialization before, To infuse we implement ResponseBodyAdvice requestResponseBodyAdvice list.

See the Bean instantiation source in AbstractAutowireCapableBeanFactory. InitializeBean () method:

As you can see, before the initialization, calls the applyBeanPostProcessorsBeforeInitialization () method, do some operations support before initialization.

In its implementation, call the BeanProcessor postProcessBeforeInitialization (result, beanName) method.

Spring container startup principles (part 2)-Bean instance creation and dependency injection

Familiarize yourself with Spring hook methods and hook interfaces to simplify your development

Therefore, we only need to implement BeanProcessor interface, and rewrite postProcessBeforeInitialization () method, in this will we achieve ResponseBodyAdvice injected. We put them together for convenience. Code implementation is as follows:

public class AppResponseBodyAdvice implements ResponseBodyAdvice.BeanPostProcessor {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        final String header = request.getHeader(AdapterRestResponseMessageProvider.HEADER);
        // Return true if the request header is app-Adapter =open and the return type is not Result
        returnObjects.equals(header, AdapterRestResponseMessageProvider.HEADER_VALUE) && ! returnType.getGenericParameterType().equals(Result.class); }@Override
    @SneakyThrows
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // The String type cannot be wrapped directly, so it needs special treatment
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(Result.ok(body));
        }
        // Wrap the original data in Result
        return Result.ok(body);
    }

    @Override
    @SneakyThrows
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        
        / / into RequestMappingHandlerAdapter requestResponseBodyAdvice attributes
        if (bean instanceof RequestMappingHandlerAdapter) {
            RequestMappingHandlerAdapter requestMappingHandlerAdapter = (RequestMappingHandlerAdapter) bean;
            final Field requestResponseBodyAdvice = FieldUtils.getField(RequestMappingHandlerAdapter.class, "requestResponseBodyAdvice".true);
            final List<Object> list = (List<Object>) FieldUtils.readField(requestResponseBodyAdvice, requestMappingHandlerAdapter);
            List<Object> temp = new ArrayList<>(list);
            list.clear();
            list.add(this);
            list.addAll(temp);
        }
        return bean;
    }

    @Data
    public static class Result<T> {

        public static final int OK = 0;

        /** * Returns code 200 as normal */
        private int code;

        /** * messages */
        private String message;

        /** * return timestamp */
        private long timestamp;

        /** * Return data */
        private T data;


        public static <T> Result<T> ok(T data) {
            Result<T> re = new Result<T>();
            re.code = OK;
            re.message = "ok";
            re.data = data;
            re.timestamp = System.currentTimeMillis();
            returnre; }}}Copy the code

The final configuration is auto-assembly, and all subsequent services simply introduce the dependency.

@Configuration
public class AppResponseBodyAdviceAutoConfiguration {
    
    @Bean
    public AppResponseBodyAdvice appResponseBodyAdvice(a) {
        return newAppResponseBodyAdvice(); }}Copy the code

Configure in spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lianjia.confucius.somehelp.response.AppResponseBodyAdviceAutoConfiguration
Copy the code

Perfect finish!!

It is not easy to be original. If you think you have written a good article, click 👍 to encourage you

Welcome to my open source project: a lightweight HTTP invocation framework for SpringBoot