Secret behind Spring ResponseBody

I. Introduction

In the stage of more and more advocating out of the box use, many details are hidden behind us, especially after we start to use spring boot, more automatic configuration makes us convenient, but also makes us think more deeply.

In this article, we will learn more about the problems related to the response body process.

II. Core

Spring processes the ResponseBody process with the following 7 classes and interfaces:

RequestMappingHandlerMapping
RequestMappingHandlerAdapter

HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler

RequestResponseBodyMethodProcessor
AbstractMessageConverterMethodProcessor

HttpMessageConverter

RequestMappingHandlerMapping is a HandlerMapping. In short, it helps us find the @ RequestMapping annotation method in the Controller that matches the request URL.

The RequestMappingHandlerAdapter can be regarded as an adapter of the @ RequestMapping annotation method in the Controller, which deals with some request and return details, such as parameter injection and return value processing.

HandlerMethodArgumentResolver is used to process the method parameters of @ RequestMapping annotation in the Controller HandlerMethodReturnValueHandler is used to process the method return value of @ RequestMapping annotation in the Controller

RequestResponseBodyMethodProcessor implements HandlerMethodArgumentResolver and HandlerMethodReturnValueHandler to handle RequestBody and ResponseBody annotations.

@The RestController class annotation will automatically add @ ResponseBody to the method, so it is unnecessary to add @ ResponseBody annotation to the Controller annotated by @ RestController.

HttpMessageConverter is used to convert the parameter type of request to Controller method and the return value of Controller method to response.

For example, the front-end request parameter is converted to the RequestBody annotation parameter, and the back-end Controller method's returned class is converted to the front-end json, xml, etc. which is actually completed through HttpMessageConverter.

You can use the following methods to see which classes are in the Spring container and check whether the above mentioned classes appear:

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationHolder implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        String[] names = applicationContext.getBeanDefinitionNames();
        for(String name : names){
            System.out.println(name);
        }
    }
}

III. HttpMessageConverters customization

Now that we know the previous content, we can focus on HttpMessageConverter. HttpMessageConverter is created by RequestMappingHandlerAdapter. If you are interested, you can see the constructor of RequestMappingHandlerAdapter.

Annotation driven bean definition parser parses the message converters of the xml configuration file.

If you are interested in adding HttpMessageConverter, you can take a look at the relevant source code. We will not introduce it in detail here, but we will introduce how to customize HttpMessageConverter in spring boot.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private HttpMessageConverters messageConverters;

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        List<HttpMessageConverter<?>> httpMessageConverters = this.messageConverters.getConverters();
        httpMessageConverters.forEach(System.out::println);
        converters.addAll(httpMessageConverters);

//        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//        FastJsonConfig fastJsonConfig = new FastJsonConfig();
//        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
//        List<MediaType> fastMediaTypes =  new ArrayList<>();
//        fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//        fastConverter.setSupportedMediaTypes(fastMediaTypes);
//        fastConverter.setFastJsonConfig(fastJsonConfig);
//        converters.add(fastConverter);
    }

}

Through the configureMessageConverters method of WebMvcConfigurer, we can freely view those httpmessageconverters, which can also be easily added and deleted.

For example, if we don't want to use the default mappingjackson 2httpmessageconverter, we can delete it and add a FastJsonHttpMessageConverter.

If you are not satisfied, you can even create an HttpMessageConverters by yourself, replacing the default:

 @Bean
public HttpMessageConverters customConverters(){
    StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
    stringHttpMessageConverter.setWriteAcceptCharset(false);
    ByteArrayHttpMessageConverter byteArrayHttpMessageConverter = new ByteArrayHttpMessageConverter();
    SourceHttpMessageConverter<Source> sourceSourceHttpMessageConverter = new SourceHttpMessageConverter<>();
    AllEncompassingFormHttpMessageConverter allEncompassingFormHttpMessageConverter = new AllEncompassingFormHttpMessageConverter();
    return new HttpMessageConverters(stringHttpMessageConverter ,byteArrayHttpMessageConverter,sourceSourceHttpMessageConverter,allEncompassingFormHttpMessageConverter);
}

Or we don't want to change it. We just want to configure the default Jackson behavior:

@Bean
public ObjectMapper jsonMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper mapper = builder.createXmlMapper(false).build();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    return mapper;
}

Four. Example

The following classes are placed in the SpringBoot project to execute the final test class.

import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

@XmlRootElement
public class Result<T> implements Serializable {
    
    private static final long serialVersionUID = -1;

    /**
     * The client can judge whether it is successful or not
     */
    private boolean status = false;

    /**
     * Status code, convenient and fast location
     */
    private int code;

    /**
     * Prompt information
     */
    private String msg;

    /**
     * data
     */
    private T data;

    public Result() {
    }

    public Result(boolean status, int code, String msg) {
        this.status = status;
        this.code = code;
        this.msg = msg;
    }

    public Result(boolean status, int code, String msg, T data) {
        this.status = status;
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public boolean isStatus() {
        return status;
    }

    public void setStatus(boolean status) {
        this.status = status;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
public final class ResultHelper {

    private static final boolean DEFAULT_SUCCESS_STATUS = true;

    private static final boolean DEFAULT_FAIL_STATUS = false;

    private static final int DEFAULT_SUCCESS_CODE = 200;

    private static final int PARAM_ERROR_CODE = 400;

    private static final int DEFAULT_ERROR_CODE = 600;

    private static final String DEFAULT_SUCCESS_MESSAGE = "success";

    private static final String DEFAULT_ERROR_MESSAGE = "fail";

    private static final String PARAM_ERROR_MESSAGE = "Parameter error";


    public static <T> Result getDefaultSuccessResult(T data){
        return new Result(DEFAULT_SUCCESS_STATUS,DEFAULT_SUCCESS_CODE,DEFAULT_SUCCESS_MESSAGE,data);
    }

    public static Result getDefaultErrorResult(){
        return new Result(DEFAULT_FAIL_STATUS, DEFAULT_ERROR_CODE, DEFAULT_ERROR_MESSAGE);
    }

    public static Result getParamErrorResult(){
        return new Result(DEFAULT_FAIL_STATUS,PARAM_ERROR_CODE,PARAM_ERROR_MESSAGE);
    }

    public static Result getErrorResult(int code,String message){
        return new Result(DEFAULT_FAIL_STATUS, code, message);
    }
}
import org.curitis.common.Result;
import org.curitis.common.ResultHelper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/rrb")
public class RRBController {

    @RequestMapping("/hello")
    public Result hello(){
        return ResultHelper.getParamErrorResult();
    }
    
}
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@SpringBootTest
public class RRBControllerTest {

    @Autowired
    protected WebApplicationContext wac;

    protected MockMvc mockMvc;

    @Before
    public void setUp(){
        this.mockMvc = webAppContextSetup(this.wac).build();
    }

    @Test
    public void json() throws Exception {
        mockMvc.perform(post("/rrb/hello"))
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();
    }

    @Test
    public void xml() throws Exception {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(HttpHeaders.ACCEPT, "application/xml;charset=UTF-8");
        mockMvc.perform(post("/rrb/hello")
                .headers(httpHeaders)
                .param("page","1")
                .param("pageSize","20")
        )
                .andDo(new ResultHandler() {
                    @Override
                    public void handle(MvcResult result) throws Exception {
                        System.out.println(result.getResponse().getContentAsString());
                    }
                })
                .andReturn();
    }

}

Execute the above test class, we can see that we can get different output, json will get json string, xml will get xml string.

It's just because the accept of our request header is different.

Note that to use jaxb2rooterelementhttpmessageconverter, you need to add @ XmlRootElement annotation on the returned entity class.

Five, process

The specific code will not be described in detail, but the process and key code. Interested friends can debug by themselves.

After the method return in the Controller, if the method has the @ ResponseBody annotation, it will come to the handleReturnValue of the RequestResponseBodyMethodProcessor.

handleReturnValue calls the writeWithMessageConverters method of AbstractMessageConverterMethodProcessor.

When the breakpoint is set to the writeWithMessageConverters method, you need to track that and see that. You don't need to be confused by other classes and processes.

The important logic in the writeWithMessageConverters method is to find the MediaType, because HttpMessageConverter should be found according to the MediaType.

First of all, judging from the content type of Response, if there is one, it means that the server is very clear about what type to return, so it can be determined directly.

If not, it will be found according to the request. If there is no special configuration, it is to see the Accept in the Header. The class involved is HeaderContentNegotiationStrategy.

Finding what the client needs is not complete, because what matters is not what you want, but what I have.

Therefore, we need to find out which mediatypes the application supports, which is basically to traverse all the types supported by HttpMessageConverters in HttpMessageConverters.

Finding the supported mediatypes will sort. How?

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

When we look at the request header, we often see the above content. The part with q=0.9 can be regarded as the weight. The higher the value, the higher the priority. For example, the above application/xml;q=0.9,/;q=0.8 means that xml has higher priority than all type.

If all the mediatypes supported by HttpMessageConverter do not match, you will get the following exception:

HttpMessageNotWritableException:No converter found for return value of type

Six, summary

Spring boot will use mappingjackson 2httpmessageconverter by default. If we don't want to use other JSON converters, we can customize Jackson by adding an ObjectMapper.

@Bean
public ObjectMapper jsonMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper mapper = builder.createXmlMapper(false).build();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    return mapper;
}

For Jackson's configuration, please refer to: Jackson's most commonly used configuration and annotation

Tags: Programming xml Spring JSON Junit

Posted on Wed, 06 Nov 2019 19:56:42 -0500 by VapiD