HttpMessageConverter是這樣轉換數據的

         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)。這幾個方法先有個印象即可

public interface HttpMessageConverter<T> {

	/**
	 * Indicates whether the given class can be read by this converter.
	 * @param clazz the class to test for readability
	 * @param mediaType the media type to read (can be {@code null} if not specified);
	 * typically the value of a {@code Content-Type} header.
	 * @return {@code true} if readable; {@code false} otherwise
	 */
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

	/**
	 * Indicates whether the given class can be written by this converter.
	 * @param clazz the class to test for writability
	 * @param mediaType the media type to write (can be {@code null} if not specified);
	 * typically the value of an {@code Accept} header.
	 * @return {@code true} if writable; {@code false} otherwise
	 */
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

	/**
	 * Return the list of {@link MediaType} objects supported by this converter.
	 * @return the list of supported media types
	 */
	List<MediaType> getSupportedMediaTypes();

	/**
	 * Read an object of the given type from the given input message, and returns it.
	 * @param clazz the type of object to return. This type must have previously been passed to the
	 * {@link #canRead canRead} method of this interface, which must have returned {@code true}.
	 * @param inputMessage the HTTP input message to read from
	 * @return the converted object
	 * @throws IOException in case of I/O errors
	 * @throws HttpMessageNotReadableException in case of conversion errors
	 */
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

	/**
	 * Write an given object to the given output message.
	 * @param t the object to write to the output message. The type of this object must have previously been
	 * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
	 * @param contentType the content type to use when writing. May be {@code null} to indicate that the
	 * default content type of the converter must be used. If not {@code null}, this media type must have
	 * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
	 * returned {@code true}.
	 * @param outputMessage the message to write to
	 * @throws IOException in case of I/O errors
	 * @throws HttpMessageNotWritableException in case of conversion errors
	 */
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}
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 stringHttpMessageConverter = new StringHttpMessageConverter();
		stringHttpMessageConverter.setWriteAcceptCharset(false);  // see SPR-7316

		messageConverters.add(new ByteArrayHttpMessageConverter());
		messageConverters.add(stringHttpMessageConverter);
		messageConverters.add(new ResourceHttpMessageConverter());
		messageConverters.add(new ResourceRegionHttpMessageConverter());
		try {
			messageConverters.add(new SourceHttpMessageConverter<>());
		}
		catch (Throwable ex) {
			// Ignore when no TransformerFactory implementation is available...
		}
		messageConverters.add(new AllEncompassingFormHttpMessageConverter());

		if (romePresent) {
			messageConverters.add(new AtomFeedHttpMessageConverter());
			messageConverters.add(new RssChannelHttpMessageConverter());
		}

		if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
		}
		else if (jaxb2Present) {
			messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
		}

		if (jackson2Present) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
		}
		else if (gsonPresent) {
			messageConverters.add(new GsonHttpMessageConverter());
		}
		else if (jsonbPresent) {
			messageConverters.add(new JsonbHttpMessageConverter());
		}

		if (jackson2SmilePresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
		}
		if (jackson2CborPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
		}
	}

我們看到很熟悉的 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);

我將進入 ha.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;
    }
}

 

上面代碼第5行,我們看到有這樣代碼:

outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                    inputMessage, outputMessage);

其實,我們在設計 RESTful API 接口的時候通常會將返回的數據封裝成統一格式,通常我們會實現 ResponseBodyAdvice 接口來處理所有 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);
    }
}

至此,通過 HttpMessageConverter 轉換請求和響應數據的流程就是這樣,整個實現過程細節還需小夥伴自行追蹤發現(一定要親自嘗試),在文章開頭我們說過添加自己的 MessageConverter 能更好的滿足我們的定製化,都有哪些內容可以定製的呢?

定製化

空值處理

請求和返回的數據有很多空值,這些值有時候並沒有實際意義,我們可以過濾掉和不返回,或設置成默認值。比如通過重寫 getObjectMapper 方法,將返回結果的空值不進行序列化處理:

@EnableWebMvc
@Configuration
public class MyWebMvcConfig extends WebMvcConfigurerAdapter {
  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(0, new MappingJackson2HttpMessageConverter(){
        @Override
        public ObjectMapper getObjectMapper() {
            super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
                    return super.getObjectMapper();
        }
    }
  }
}

XSS 腳本攻擊

爲了確保輸入的數據更安全,防止 XSS 腳本攻擊,我們可以添加自定義的反序列化器:


@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    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 屬性的值。如果服務端支持這個 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 FormHttpMessageConverter Map<K, List<?>> application/x-www-form-urlencoded, multipart/form-data
SourceHttpMessageConverter Source application/xml, text/xml, application/*+xml

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章