springMVC的消息轉換器(Message Converter)

簡介

請求和響應都有對應的body,而這個body就是需要關注的主要數據。

請求體與請求的查詢參數或者表單參數是不同的,請求體的表述一般就是一段字符串,而查詢參數可以看作url的一部分,這兩個是位于請求報文的不同地方。表單參數可以按照一定格式放在請求體中,也可以放在url上作為查詢參數。總之可以把請求體看作客戶端通過請求報文捎帶的字符串。

響應體則是瀏覽器渲染頁面的依據,對于一個普通html頁面得響應,響應體就是這個html頁面的源代碼。

請求體和響應體都是需要配合Content-Type頭部使用的,這個頭部主要用于說明body中得字符串是什么格式的,比如:text,json,xml等。對于請求報文,只有通過此頭部,服務器才能知道怎么解析請求體中的字符串,對于響應報文,瀏覽器通過此頭部才知道應該怎么渲染響應結果,是直接打印字符串還是根據代碼渲染為一個網頁。

還有一個與body有關的頭部是Accept,這個頭部標識了客戶端期望得到什么格式的響應體。服務器可根據此字段選擇合適的結果表述。

對于HttpServletRequest和HttpServletResponse,可以分別調用getInputStream和getOutputStream來直接獲取body。但是獲取到的僅僅只是一段字符串,而對于java來說,處理一個對象肯定比處理一個字符串要方便得多,也好理解得多。所以根據Content-Type頭部,將body字符串轉換為java對象是常有的事。反過來,根據Accept頭部,將java對象轉換客戶端期望格式的字符串也是必不可少的工作。

spring消息轉換器源碼簡要分析

而springMVC為我們提供了一系列默認的消息轉換器。

對于消息轉換器的調用,都是在RequestResponseBodyMethodProcessor類中完成的。它實現了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler兩個接口,分別實現了處理參數和處理返回值的方法。

而要動用這些消息轉換器,需要在特定的位置加上@RequestBody和@ResponseBody。

/**
 * RequestResponseBodyMethodProcessor.class
 */
···
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }

    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                returnType.hasMethodAnnotation(ResponseBody.class));
    }
···

對返回值的消息轉換來說:

/**
 * RequestResponseBodyMethodProcessor.class
 */
···
public void handleReturnValue(Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException;

大致流程為:
1.根據返回值獲取其類型,其中MethodParameter封裝了方法對象,可獲去方法返回值類型。

/**
 * AbstractMessageConverterMethodProcessor.class
 */
···
            outputValue = value;
            valueType = getReturnValueType(outputValue, returnType);
            declaredType = getGenericType(returnType);

2.根據request的Accept和HandellerMapping的produces屬性經過比對、排序從而得到最應該轉換的消息格式(MediaType)。

/**
 * AbstractMessageConverterMethodProcessor.class
 */
···
        List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
        List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
        Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
···
//匹配
···
        List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
        MediaType.sortBySpecificityAndQuality(mediaTypes);

        MediaType selectedMediaType = null;
···
//選擇最具體的MediaType
···

3.遍歷所有已配置的消息轉換器,調用其canWrite方法,根據返回值類型(valueType)和消息格式(MediaType)來檢測是否可以轉換。

/**
 * AbstractMessageConverterMethodProcessor.class
 */
···
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
···
     if (messageConverter.canWrite(valueType, selectedMediaType)){
···
        ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
···
     }
}

4.若有對應的轉換器,則執行消息轉換,即write方法。在write方法中,將返回值被轉換后得到的字符串寫在Response的輸出流中。若找不到對應的轉換器,則拋出HttpMediaTypeNotAcceptableException異常,瀏覽器會收到一個406 Not Acceptable狀態碼。

自定義消息轉換器

除了spring提供的9個默認的消息轉換器,還可以添加自定義的消息轉換器,或者更換消息轉換器的實現。

一個自定義消息轉換器的例子:
該例子旨在將json轉換器替換為fastjson實現,xml轉換器替換為jackson-dataformat-xml實現。

首先添加依賴:

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.8.7</version>
        </dependency>

配置類:

@Configuration
public class Cfg_Web {
    //message converter
    @Bean
    public HttpMessageConverters messageConverters(){
        //json
        FastJsonHttpMessageConverter jsonMessageConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setCharset(Charset.forName("utf-8"));
        jsonMessageConverter.setFastJsonConfig(fastJsonConfig);
        List<MediaType> jsonMediaTypes = new ArrayList<>();
        jsonMediaTypes.add(MediaType.APPLICATION_JSON);
        jsonMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        jsonMessageConverter.setSupportedMediaTypes(jsonMediaTypes);

        //xml
        MappingJackson2XmlHttpMessageConverter xmlMessageConverter = new MappingJackson2XmlHttpMessageConverter();
        xmlMessageConverter.setObjectMapper(new XmlMapper());
        xmlMessageConverter.setDefaultCharset(Charset.forName("utf-8"));
        List<MediaType> xmlMediaTypes = new ArrayList<>();
        xmlMediaTypes.add(MediaType.APPLICATION_XML);
        xmlMediaTypes.add(MediaType.TEXT_XML);
        xmlMessageConverter.setSupportedMediaTypes(xmlMediaTypes);

        return new HttpMessageConverters(Arrays.asList(jsonMessageConverter, xmlMessageConverter));
    }
}

測試使用:

public class Student {
    private String code;
    private String name;
···//省略set和get方法
    @Override
    public String toString() {
        return "Student{" +
                "code='" + code + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}
@Controller
public class TestController {

    @RequestMapping(value = "json", produces = MediaType.APPLICATION_XML_VALUE)
    @ResponseBody
    public Object jsonTest(@RequestBody Student student){
        System.out.println(student);

        return student;
    }

    @RequestMapping(value = "xml", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public Object xmlTest(@RequestBody Student student){
        System.out.println(student);

        return student;
    }
}

這個測試主要是將json格式和xml格式請求響應互轉。

json -> xml
xml -> json

一些小細節

1.如果一個Controller類里面所有方法的返回值都需要經過消息轉換器,那么可以在類上面加上@ResponseBody注解或者將@Controller注解修改為@RestController注解,這樣做就相當于在每個方法都加上了@ResponseBody注解了。
2.@ResponseBody@RequestBody都可以處理Map類型的對象。如果不確定參數的具體字段,可以用Map接收。@ReqeustBody同樣適用。
3.方法上的和類上的@ResponseBody都可以被繼承。
4.默認的xml轉換器Jaxb2RootElementHttpMessageConverter需要類上有@XmlRootElement注解才能被轉換。

/**
* Jaxb2RootElementHttpMessageConverter.class
*/
    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return (AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && canWrite(mediaType));
    }

5.返回值類型可聲明為基類的類型,不影響轉換,但參數的類型必需為特定的類型。這是顯而易見的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容