Java Web 人員經常要設計 RESTful API(如何設計好的RESTful API),通過 json 數據進行交互。那麼前端傳入的 json 數據如何被解析成 Java 對象作爲 API入參,API 返回結果又如何將 Java 對象解析成 json 格式數據返回給前端,其實在整個數據流轉過程中,HttpMessageConverter
起到了重要作用;另外在轉換的過程我們可以加入哪些定製化內容?
HttpMessageConverter 介紹
org.springframework.http.converter.HttpMessageConverter
是一個策略接口,接口說明如下:
Strategy interface that specifies a converter that can convert from and to HTTP requests and responses. 簡單說就是 HTTP request (請求)和response (響應)的轉換器。該接口有隻有5個方法,簡單來說就是獲取支持的 MediaType(application/json之類),接收到請求時判斷是否能讀(canRead),能讀則讀(read);返回結果時判斷是否能寫(canWrite),能寫則寫(write)。這幾個方法先有個印象即可:
boolean canRead(Class<?> clazz, MediaType mediaType);
boolean canWrite(Class<?> clazz, MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
缺省配置
通常我們寫 Demo 沒有配置任何 MessageConverter,但是數據前後傳遞依舊好用,是因爲 SpringMVC 啓動時會自動配置一些HttpMessageConverter,在 WebMvcConfigurationSupport
類中添加了缺省 MessageConverter:
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter<Source>());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build();
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
}
我們看到很熟悉的 MappingJackson2HttpMessageConverter
,如果我們引入 jackson 相關包,Spring 就會爲我們添加該 MessageConverter,但是我們通常在搭建框架的時候還是會手動添加配置 MappingJackson2HttpMessageConverter
,爲什麼? 先思考一下:
當我們配置了自己的 MessageConverter, SpringMVC 啓動過程就不會調用
addDefaultHttpMessageConverters
方法,且看下面代碼if
條件,這樣做也是爲了定製化我們自己的 MessageConverter
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
類關係圖
在此處僅列出 MappingJackson2HttpMessageConverter
和 StringHttpMessageConverter
兩個轉換器,我們發現, 前者實現了 GenericHttpMessageConverter
接口, 而後者卻沒有,留有這個關鍵印象,這是數據流轉過程中關鍵邏輯判斷
數據流轉解析
數據的請求和響應都要經過 DispatcherServlet
類的 doDispatch(HttpServletRequest request, HttpServletResponse response)
方法的處理
請求過程解析
看 doDispatch 方法中的關鍵代碼:
// 這裏的 Adapter 實際上是 RequestMappingHandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 實際處理的handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mappedHandler.applyPostHandle(processedRequest, response, mv);
從進入handle之後我先將調用棧粘貼在此處,希望小夥伴可以按照調用棧路線動手跟蹤嘗試:
readWithMessageConverters:192, AbstractMessageConverterMethodArgumentResolver (org.springframework.web.servlet.mvc.method.annotation)
readWithMessageConverters:150, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:128, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:121, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:158, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:128, InvocableHandlerMethod (org.springframework.web.method.support)
// 下面的調用棧重點關注,處理請求和返回值的分叉口就在這裏
invokeAndHandle:97, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:849, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:760, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:967, DispatcherServlet (org.springframework.web.servlet)
這裏重點說明調用棧最頂層 readWithMessageConverters
方法中內容:
// 遍歷 messageConverters
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
// 上文類關係圖處要重點記住的地方,主要判斷 MappingJackson2HttpMessageConverter 是否是 GenericHttpMessageConverter 類型
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = genericConverter.read(targetType, contextClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
else if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
}
if (inputMessage.getBody() != null) {
inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
}
break;
}
}
}
然後就判斷是否canRead,能讀就read,最終走到下面代碼處將輸入的內容反序列化出來:
protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
throws IOException
{
try (JsonParser p = p0) {
Object result;
JsonToken t = _initForReading(p);
if (t == JsonToken.VALUE_NULL) {
// Ask JsonDeserializer what 'null value' to use:
DeserializationContext ctxt = createDeserializationContext(p,
getDeserializationConfig());
result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else {
DeserializationConfig cfg = getDeserializationConfig();
DeserializationContext ctxt = createDeserializationContext(p, cfg);
JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
if (cfg.useRootWrapping()) {
result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
} else {
result = deser.deserialize(p, ctxt);
}
ctxt.checkUnresolvedObjectId();
}
// Need to consume the token too
p.clearCurrentToken();
return result;
}
}
到這裏從請求中解析參數過程就到此結束了,趁熱打鐵來看將響應結果返回給前端的過程
返回過程解析
在上面調用棧請求和返回結果分叉口處同樣處理返回的內容:
writeWithMessageConverters:224, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support)
// 分叉口
invokeAndHandle:113, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
重點關注調用棧頂層內容,是不是很熟悉的樣子,完全一樣的邏輯, 判斷是否能寫canWrite,能寫則write:
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
我們看到有這樣一行代碼:
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
我們在設計 RESTful API 接口的時候通常會將返回的數據封裝成統一格式,通常我們會實現 ResponseBodyAdvice<T> 接口來處理所有 API 的返回值,在真正 write 之前將數據進行統一的封裝
@RestControllerAdvice()
public class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof CommonResult) {
return body;
}
return new CommonResult<Object>(body);
}
}
整個處理流程就是這樣,整個實現過程細節還需小夥伴自行追蹤發現,文章開頭我們說過 添加自己的 MessageConverter 能更好的滿足我們的定製化,都有哪些可以定製的呢?
定製化
空值處理
請求和返回的數據有很多空值,這些值有時候並沒有實際意義,我們可以過濾掉和不返回,或設置成默認值。比如通過重寫 getObjectMapper
方法,將返回結果的空值不進行序列化:
converters.add(0, new MappingJackson2HttpMessageConverter(){
@Override
public ObjectMapper getObjectMapper() {
super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
return super.getObjectMapper();
}
}
XSS 腳本攻擊
爲了保證輸入的數據更安全,防止 XSS 腳本攻擊,我們可以添加自定義反序列化器:
//對應無法直接返回String類型
converters.add(0, new MappingJackson2HttpMessageConverter(){
@Override
public ObjectMapper getObjectMapper() {
super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
// XSS 腳本過濾
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(String.class, new StringXssDeserializer());
super.getObjectMapper().registerModule(simpleModule);
return super.getObjectMapper();
}
}
細節分析
canRead 和 canWrite 的判斷邏輯是什麼呢? 請看下圖:
客戶端 Request Header 中設置好 Content-Type(傳入的數據格式)和Accept(接收的數據格式),根據配置好的MessageConverter來判斷是否 canRead 或 canWrite,然後決定 response.body 的 Content-Type 的第一要素是對應的request.headers.Accept 屬性的值,又叫做 MediaType。如果服務端支持這個 Accept,那麼應該按照這個 Accept 來確定返回response.body 對應的格式,同時把 response.headers.Content-Type 設置成自己支持的符合那個 Accept 的 MediaType
總結與思考
站在上帝視角看,整個流程可以按照下圖進行概括,請求報文先轉換成 HttpInputMessage, 然後再通過 HttpMessageConverter 將其轉換成 SpringMVC 的 java 對象,反之亦然。
將各種常用 HttpMessageConverter 支持的MediaType 和 JavaType 以及對應關係總結在此處:
類名 | 支持的JavaType | 支持的MediaType |
---|---|---|
ByteArrayHttpMessageConverter | byte[] | application/octet-stream, */* |
StringHttpMessageConverter | String | text/plain, */* |
MappingJackson2HttpMessageConverter | Object | application/json, application/*+json |
AllEncompassingFormHttpMessageConverter | Map<K, List<?>> | application/x-www-form-urlencoded, multipart/form-data |
SourceHttpMessageConverter | Source | application/xml, text/xml, application/*+xml |
最後思考這樣一個問題:爲什麼 HttpMessageConverter 在寫的過程中,先判斷 canWrite 後判斷是否有 responseBodyAdvice 的數據封裝呢? 如果先進行 responseBodyAdvice 的數據封裝後判斷 canWrite 會怎樣呢?
提高效率工具
依舊介紹寫該文章用到的一些好的工具
processon
ProcessOn是一個在線作圖工具的聚合平臺,它可以在線畫流程圖、思維導圖、UI原型圖、UML、網絡拓撲圖、組織結構圖等等,
您無需擔心下載和更新的問題,不管Mac還是Windows,一個瀏覽器就可以隨時隨地的發揮創意,規劃工作,同時您可以把作品分享給團隊成員或好友,無論何時何地大家都可以對作品進行編輯、閱讀和評論
SequenceDiagram
SequenceDiagram 是 IntelliJ IDEA 的一個插件,有了這個插件,你可以
- 生成簡單序列圖。
- 單擊圖形形狀來導航代碼。
- 從圖中刪除類。
- 將圖表導出爲圖像。
-
通過“設置”>“其他設置”>“序列”從圖表中排除類
方便快速的定位方法和理解類的調用過程