隐藏在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最经常使用配置与注解

相关文章
相关标签/搜索