簡介
請求和響應都有對應的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格式請求響應互轉。
一些小細節
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.返回值類型可聲明為基類的類型,不影響轉換,但參數的類型必需為特定的類型。這是顯而易見的。