mvc:message-converters簡單介紹
說說@ResponseBody註解,很明顯這個註解就是將方法的返回值作為reponse的body部分。我們進一步分析下這個過程涉及到的內容,首先就是方法返回的類型,可以是字節數組、字符串、對象引用等,將這些返回類型以什麽樣的內容格式(即response的content-type類型,同時還要考慮到客戶端是否接受這個類型)存進response的body中返回給客戶端是一個問題,對於這個過程的處理都是靠許許多多的HttpMessageConverter轉換器來完成的,這便是本章要講的內容。
常用的content-type類型有:text/html、text/plain、text/xml、application/json、application/x-www-form-urlencoded、image/png等,不同的類型,對body中的數據的解析也是不一樣的。
我們的@ResponseBody可以指定content-type,打開ResponseBody註釋,我們可以看到這兩個屬性consumes和produces,它們就是用來指定request的content-type和response的content-type的。都可以接收一個或者多個,用法註釋中已經給出了說明。
/* The consumable media types of the mapped request, narrowing the primary mapping. * <p>The format is a single media type or a sequence of media types, * with a request only mapped if the {@code Content-Type} matches one of these media types. * Examples: * <pre class="code"> * consumes = "text/plain" * consumes = {"text/plain", "application/*"} * </pre> * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches * all requests with a {@code Content-Type} other than "text/plain". * <p><b>Supported at the type level as well as at the method level!</b> * When used at the type level, all method-level mappings override * this consumes restriction. * @see org.springframework.http.MediaType * @see javax.servlet.http.HttpServletRequest#getContentType()*/ String[] consumes() default {}; /** * The producible media types of the mapped request, narrowing the primary mapping. * <p>The format is a single media type or a sequence of media types, * with a request only mapped if the {@code Accept} matches one of these media types. * Examples: * <pre class="code"> * produces = "text/plain" * produces = {"text/plain", "application/*"} * </pre> * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches * all requests with a {@code Accept} other than "text/plain". * <p><b>Supported at the type level as well as at the method level!</b> * When used at the type level, all method-level mappings override * this consumes restriction. * @see org.springframework.http.MediaType */ String[] produces() default {};
當request的content-type不在consumes指定的範圍內,則這個request就不會匹配到這個方法。produces 同時指定了方法的返回值將以什麽樣的content-type寫入response的body中。如果這個屬性進行了配置下文在獲取服務器端指定的content-type就是所配置的值,否則則會獲取默認的所有content-type
當我們對@ResponseBody什麽都沒有配置時,SpringMVC便啟用默認的策略幫我們自動尋找一種最佳的方式將方法的返回值寫入response的body中。
接下來,我們就需要探究SpringMVC是如何處理這一過程的。先說說我的方式,就是調試,當方法執行完返回後,看DispatcherServlet的doDispatch方法的代碼:
// Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } try { //這裏就是執行我們的業務邏輯,並且對返回結果進行處理的地方 // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); }
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());這裏是適配器進行調度我們的handler地方,由於我們使用的是註解,所以對應的適配器是RequestMappingHandlerAdapter,通過一步步的函數調用,最終找到我們的關註重點到RequestMappingHandlerAdapter的方法invokeHandleMethod,具體實現邏輯:
private ModelAndView invokeHandleMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, requestMappingMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncWebRequest.setTimeout(this.asyncRequestTimeout); final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.setTaskExecutor(this.taskExecutor); asyncManager.setAsyncWebRequest(asyncWebRequest); asyncManager.registerCallableInterceptors(this.callableInterceptors); asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); if (logger.isDebugEnabled()) { logger.debug("Found concurrent result value [" + result + "]"); } requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result); } //這裏是重點,執行handler的業務邏輯,對於@ResponseBody分支的處理在這裏 requestMappingMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } //這裏便是分水嶺,要麽返回一個ModelAndView,對於@ResponseBody的返回內容已寫進response的body中,在這裏要返回null。 return getModelAndView(mavContainer, modelFactory, webRequest); }
繼續深入看下這個requestMappingMethod.invokeAndHandle方法:
/** * Invokes the method and handles the return value through a registered * {@link HandlerMethodReturnValueHandler}. * * @param webRequest the current request * @param mavContainer the ModelAndViewContainer for this request * @param providedArgs "given" arguments matched by type, not resolved */ public final void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { //這裏執行完方法體,並返回結果內容 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); setResponseStatus(webRequest); if (returnValue == null) { if (isRequestNotModified(webRequest) || hasResponseStatus() || mavContainer.isRequestHandled()) { mavContainer.setRequestHandled(true); return; } } else if (StringUtils.hasText(this.responseReason)) { mavContainer.setRequestHandled(true); return; } mavContainer.setRequestHandled(false); try { //重點在這裏 this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { if (logger.isTraceEnabled()) { logger.trace(getReturnValueHandlingErrorMessage("Error handling return value", returnValue), ex); } throw ex; } }
mavContainer是ModelAndViewContainer類型,主要存儲著model信息和view信息,它的一個屬性requestHandled為true表示response直接處理不需要view的解決方案(即是需要返回一個視圖的)。這裏的mavContainer.setRequestHandled(false)只是初始時默認采用view的解決方案。
繼續看this.returnValueHandlers.handleReturnValue具體內容:
/**這裏已經寫了,要遍歷已經註冊的HandlerMethodReturnValueHandler,然後執行那個支持returnValue的那一個HandlerMethodReturnValueHandler * Iterate over registered {@link HandlerMethodReturnValueHandler}s and invoke the one that supports it. * @exception IllegalStateException if no suitable {@link HandlerMethodReturnValueHandler} is found. */ @Override public void handleReturnValue( Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType); Assert.notNull(handler, "Unknown return value type [" + returnType.getParameterType().getName() + "]"); handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } 繼續看下它是如何找到合適的HandlerMethodReturnValueHandler的 Java代碼 收藏代碼 /** * Find a registered {@link HandlerMethodReturnValueHandler} that supports the given return type. */ private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { for (HandlerMethodReturnValueHandler returnValueHandler : returnValueHandlers) { if (logger.isTraceEnabled()) { logger.trace("Testing if return value handler [" + returnValueHandler + "] supports [" + returnType.getGenericParameterType() + "]"); } if (returnValueHandler.supportsReturnType(returnType)) { return returnValueHandler; } } return null; }
遍歷所有的已註冊的HandlerMethodReturnValueHandler,然後調用他們的supportsReturnType方法來判斷他們各自是否支持這個返回值類型,通過調試發現會有13個HandlerMethodReturnValueHandler,之後再說這些數據是在什麽時候哪個地方註冊的。列舉下常用的:
- ModelAndViewMethodReturnValueHandler:支持返回值是ModelAndView類型的
- ModelMethodProcessor:支持返回值是Model的
- ViewMethodReturnValueHandler:支持返回值是View
- HttpEntityMethodProcessor:支持返回值是HttpEntity
- RequestResponseBodyMethodProcess:支持類上或者方法上含有@ResponseBody註解的
- ViewNameMethodReturnValueHandler:支持返回類型是void或者String
所以我們想擴展的話,就可以自定實現一個HandlerMethodReturnValueHandler,然後在初始化時註冊進去(這個過程後面再說)。言歸正轉,對於本工程RequestResponseBodyMethodProcess是支持的,所以它將被作為HandlerMethodReturnValueHandler返回,繼續執行上面的handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest)方法,我們來看下RequestResponseBodyMethodProcess具體的處理過程:
@Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException { //走到這一步,說明該方法不需要view的方案,所以要將requestHandled標示置為true,供其他註釋使用判斷當前方法的返回值處理策略 mavContainer.setRequestHandled(true); if (returnValue != null) { writeWithMessageConverters(returnValue, returnType, webRequest); } }
方法writeWithMessageConverters的具體內容為:
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException { Class<?> returnValueClass = returnValue.getClass(); HttpServletRequest servletRequest = inputMessage.getServletRequest(); //獲取客戶端Accept字段接收的content-type List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest); //獲取服務器端指定的content-type,如果@RequestMapping中的produces配置了content-type,則返回此content-type,若果沒有,
則獲取所有HttpMessageConverter所支持的content-type,然後通過requestedMediaTypes和producibleMediaTypes 對比,選定一個最合適的content-type作為 //selectedMediaType List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass); Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); for (MediaType requestedType : requestedMediaTypes) { for (MediaType producibleType : producibleMediaTypes) { if (requestedType.isCompatibleWith(producibleType)) { compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (compatibleMediaTypes.isEmpty()) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null; for (MediaType mediaType : mediaTypes) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> messageConverter : //遍歷所有已註冊的HttpMessageConverter,選出一個支持返回值類型returnValueClass和 //selectedMediaType的HttpMessageConverter來進行寫入數據到response的body中。 this.messageConverters) { if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } return; } } } throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); }
獲取客戶端的content-type,只需解析Accept頭字段即可,獲取服務器端指定的content-type則分兩種情況,第一種情況為:你在@RequestMapping中指定了produces的content-type類型(會將這一信息存進request的屬性中,屬性名為HandlerMapping接口名+‘.producibleMediaTypes‘)如果沒指定,則第二種情況:獲取所有的已註冊的messageConverter,獲取它們所有的支持的content-type類型,並且過濾掉那些不支持returnValueClass的類型。然後在這兩組List<MediaType> requestedMediaTypes和producibleMediaTypes中進行比較匹配(這裏的比較規則也挺多的,涉及到q值,有興趣你們可以總結下),選出一個最合適的content-type,至此有了返回值類型returnValueClass和要寫進reponseBody的content-type類型,然後就是要找到一個支持這兩者的HttpMessageConverter,已註冊的HttpMessageConverter如下:
- ByteArrayHttpMessageConverter:支持返回值類型為byte[],content-type為application/octet-stream,*/*
- StringHttpMessageConverter:支持的返回值類型為String,content-type為 text/plain;charset=ISO-8859-1,*/*
- ResourceHttpMessageConverter:支持的返回值類型為Resource,content-type為 */*
- SourceHttpMessageConverter:支持的返回值類型為DomSource,SAXSource,Source,StreamSource,content-type為application/xml,text/xml,application/*+xml
- MappingJacksonHttpMessageConverter:判斷返回值能否被格式化成json,content-type為 application/json,application/*+json
- AllEncompassingFormHttpMessageConverter:支持的返回值類型為MultiValueMap,content-type為application/x-www-form-urlencoded,multipart/form-data
對於我們的工程來說,返回類型為String,選出來的最合適的content-type是text/html,並且StringHttpMessageConverter的*/*是兼容任意類型的,所以StringHttpMessageConverter會被選中,然後將返回值以text/html形式寫進response的body中。
順便說下對於content-length是這樣獲取的:
首先從指定的content-type(本工程即text/html)中獲取字符集,若能獲取到則使用該字符集,若獲取不到則使用默認的字符集,對於本工程來說,text/html不像application/json;charset=utf-8那樣含有字符集,所以將會使用StringHttpMessageConverter默認的字符集
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); private final Charset defaultCharset; private final List<Charset> availableCharsets; private boolean writeAcceptCharset = true; /** * A default constructor that uses {@code "ISO-8859-1"} as the default charset. * @see #StringHttpMessageConverter(Charset) */ public StringHttpMessageConverter() { this(DEFAULT_CHARSET); } /** * A constructor accepting a default charset to use if the requested content * type does not specify one. */ public StringHttpMessageConverter(Charset defaultCharset) { super(new MediaType("text", "plain", defaultCharset), MediaType.ALL); this.defaultCharset = defaultCharset; this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values()); }
StringHttpMessageConverter有兩個構造函數。當你沒有給它指定字符集時,使用默認的ISO-8859-1,這便是造成亂碼的一個原因,由於我們經常使用utf-8,所以可以在構造它時指定一下字符集。繼續content-length的計算,有了字符集就好辦了,計算方法為str.getBytes(字符集).length便是content-length的值,代碼如下。
if (headers.getContentLength() == -1) { Long contentLength = getContentLength(t, headers.getContentType()); if (contentLength != null) { headers.setContentLength(contentLength); } }
@Override protected Long getContentLength(String s, MediaType contentType) { Charset charset = getContentTypeCharset(contentType); try { return (long) s.getBytes(charset.name()).length; } catch (UnsupportedEncodingException ex) { // should not occur throw new IllegalStateException(ex); } }
繼續說StringHttpMessageConverter的寫入過程,
@Override protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException { if (this.writeAcceptCharset) { outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); } Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); //重點 StreamUtils.copy(s, charset, outputMessage.getBody()); }
上面的charset就是ISO-8859-1,也就是將返回的字符串以ISO-8859-1編碼寫進response的body中。至此就完成了,然後就出現了??-???這種亂碼。
下一篇文章將會詳細說說亂碼,再下一篇文章還要繼續本節遺留的很多問題,第一個HandlerMethodReturnValueHandler的來源及使用以及我們來自定義一個HandlerMethodReturnValueHandler,
第二個問題:其他HttpMessageConverter的使用以及自定義HttpMessageConverte
mvc:message-converters簡單介紹