隱藏在Spring ResponseBody以後的祕密

1、簡介

在現在愈來愈崇尚開箱即用的階段,不少細節被隱藏在了身後,特別是開始使用SpringBoot以後,更多的自動配置,讓咱們方便的同時,不少時候也讓咱們更加深刻的思考。html

本篇文章就來了解一下遇到比較多的ResponseBody流程相關的問題。前端

2、核心類

Spring處理ResponseBody流程相關的類和接口主要有下面7個:java

RequestMappingHandlerMapping
RequestMappingHandlerAdapter

HandlerMethodArgumentResolver
HandlerMethodReturnValueHandler

RequestResponseBodyMethodProcessor
AbstractMessageConverterMethodProcessor

HttpMessageConverter

RequestMappingHandlerMapping是一個HandlerMapping,簡單的來講它幫咱們找到和請求URL匹配的Controller中@RequestMapping註解的方法。web

RequestMappingHandlerAdapter能夠看做是Controller中@RequestMapping註解的方法的一個適配器,處理了一些請求和返回的細節,例如參數注入,返回值處理。spring

HandlerMethodArgumentResolver用於處理Controller中@RequestMapping註解的方法參數 HandlerMethodReturnValueHandler用於處理Controller中@RequestMapping註解的方法返回值json

RequestResponseBodyMethodProcessor實現了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler,用於處理RequestBody、ResponseBody註解。後端

@RestController這個類註解會自動爲方法添加上@ResponseBody,因此在@RestController註解的Controller中不須要再顯示的添加@ResponseBody註解。app

HttpMessageConverter用於request請求到Controller方法的參數類型,Controller方法的返回值到response之間的轉換。ide

例如前端的請求參數轉換爲RequestBody註解的參數,後端Controller方法的返回的類轉換爲前端須要的json、xml等實際就是經過HttpMessageConverter完成的。函數

能夠經過下面的方法看一下大體Spring容器中有哪些類,檢查有沒有出現上面提到的類:

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

3、HttpMessageConverters定製

知道了前面的內容,咱們如今重點關注一下HttpMessageConverter就能夠了。 HttpMessageConverter是RequestMappingHandlerAdapter建立的,有興趣能夠看一下RequestMappingHandlerAdapter的構造函數。

AnnotationDrivenBeanDefinitionParser解析xml配置文件的message-converters也會添加。

關於HttpMessageConverter添加有興趣能夠本身看一下相關源碼,這裏不詳細介紹,咱們要介紹的是在SpringBoot中要如何定製HttpMessageConverter。

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

}

經過WebMvcConfigurer的configureMessageConverters方法,咱們就能夠自由的查看有那些HttpMessageConverter,也能夠很容易添加很刪除。

例如咱們不想使用默認的MappingJackson2HttpMessageConverter,咱們就能夠把它刪除了,而後添加一個FastJsonHttpMessageConverter。

若是還不滿意,甚至能夠本身建立一個HttpMessageConverters,替換掉默認的:

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

或者咱們懶得改,只想配置一下默認的Jackson的行爲:

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

4、實例

下面的類放在SpringBoot工程中,執行最後的測試類。

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

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

    /**
     * 成功與否,客戶端快速判斷
     */
    private boolean status = false;

    /**
     * 狀態碼,方便快速定位問題
     */
    private int code;

    /**
     * 提示信息
     */
    private String msg;

    /**
     * 數據
     */
    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 = "參數錯誤";


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

}

執行上面的測試類,咱們能夠看到咱們可以獲得不一樣的輸出,json會獲得json字符串,xml會獲得xml字符串。

只是由於咱們請求頭的accept不一樣。

注意,要使用Jaxb2RootElementHttpMessageConverter須要在返回的實體類上添加@XmlRootElement註解。

5、流程

具體的代碼就不詳細說了,說一下流程和關鍵代碼,感興趣的朋友能夠本身調試。

在Controller中的方法return以後,若是方法有@ResponseBody註解,那麼就會來到RequestResponseBodyMethodProcessor的handleReturnValue。

handleReturnValue會調用AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法。

斷點打到writeWithMessageConverters方法中,須要跟蹤那個看那個,不用被其餘的類和流程繞暈了。

writeWithMessageConverters方法中很重要的邏輯就是找MediaType,由於要根據MediaType來找HttpMessageConverter。

首先從Response的Content-Type判斷,若是有,說明服務端是很是清楚要返回什麼類型,因此就能夠直接肯定。

若是沒有,就會根據request去找,若是沒有特殊配置就是看Header中的Accept,涉及的類是HeaderContentNegotiationStrategy。

找到客戶端須要什麼還不算完,由於重要的不是你要什麼,而是要看我有什麼。

因此還要找應用支持哪些MediaType,基本就是去HttpMessageConverters中的遍歷全部HttpMessageConverter支持的類型。

找到支持的MediaType會排序,怎麼排序呢?

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

咱們看請求頭,常常會看到上面的內容,q=0.9的部分就能夠能夠看做是權重,值越大優先級越高。 例如上面application/xml;q=0.9,/;q=0.8就表示xml比all類型優先級要高。

若是全部的HttpMessageConverter支持的MediaType都不匹配,那麼就會獲得下面的異常:

HttpMessageNotWritableException:No converter found for return value of type

6、總結

SpringBoot默認會使用MappingJackson2HttpMessageConverter,若是咱們不想使用其餘json converter,那麼咱們能夠經過添加一個ObjectMapper來定製Jackson。

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

關於Jackson的配置,能夠參考:Jackson最經常使用配置與註解

相關文章
相關標籤/搜索