從content-type設置看Spring MVC處理header的一個坑


我們經常需要在HttpResponse中設置一些headers,我們使用Spring MVC框架的時候我們如何給Response設置Header呢?

Sooooooooooooo easy, 看下面的代碼:

    @RequestMapping(value = "/rulelist", method = RequestMethod.GET)
    @ResponseBody
    public String getRuleList(HttpServletRequest request,
            HttpServletResponse response) {
        response.addHeader("test", "test"); 
        return service.getRuleList();
    }

通過驗證,我們可以看到test項已經被成功添加到response的頭部信息

Content-Length:	2 kilobytes
Content-Type:	text/plain;charset=ISO-8859-1
Server:	Apache-Coyote/1.1
test: test

接下來,我們希望修改Content-Type,從而統一服務器端和客戶端的內容編碼。我們繼續修改代碼,

    @RequestMapping(value = "/rulelist", method = RequestMethod.GET)
    @ResponseBody
    public String getRuleList(HttpServletRequest request,
            HttpServletResponse response) {
        response.addHeader("Content-Type", "application/json;charset=UTF-8");
        return service.getRuleList();
    }

接下來,我們驗證一下結果:

Content-Length:	2 kilobytes
Content-Type:	text/plain;charset=ISO-8859-1
Server:	Apache-Coyote/1.1

和我們預想的並一樣,response的content-type header沒有被設置成"application/json;charset=UTF-8",很令人困惑。

那么,接下來讓我們來探索下Spring MVC內部是如何處理這一過程的。首先我們先要對Spring MVC框架處理Http請求的流程有一個整體的了解。

下圖清晰地向大家展示了Spring MVC處理HTTP請求的流程,(圖片來自網絡)

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

具體流程如下:

1. DispatcherServlet接收到Request請求

2. HandlerMapping選擇一個合適的Handler處理Request請求

3-4. 選擇合適的HandlerAdapter,調用用戶編寫的Controller處理業務邏輯。(HandlerAdapter主要是幫助Spring MVC支持多種類型的Controller)

5. Controller將返回結果放置到Model中並且返回view名稱給Handler Adapter

6. DispatcherServlet選擇合適的ViewResolver來生成View對象

7-8. View對象利用Model中的數據進行渲染並返回數據

相信大家對於上面的處理流程並不陌生,上面的流程圖向我們展示了SpringMVC生成ModelAndView並返回response的大體流程。

下面我們來看看我們上面代碼片段的處理流程是如何進行的?

從上面的流程圖我們可以看到,content-type header是單獨被處理的,具體過程可以參考下面的源碼(AbstractMessageConverterMethodProcessor):

	protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
			ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
			throws IOException, HttpMediaTypeNotAcceptableException {

		Class<?> returnValueClass = getReturnValueType(returnValue, returnType);
		HttpServletRequest servletRequest = inputMessage.getServletRequest();
		List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);

//適合的兼容media types類型
實際上,我們可以使用produces = {}來指定我們需要的mediatype
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()) { if (returnValue != null) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } return; } List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null;
   //選擇最匹配的mediaType 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 : this.messageConverters) {
//遍歷messageConvertors, 尋找可以處理相應返回類型和mediatype的HttpMessageConvertor if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType, (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (returnValue != null) {
//這里將會填充mediatype到header,並將httpmessage發送給請求者 ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; } } } if (returnValue != null) { throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); } }

接下來,將選擇好的mediatype寫入到HttpOutputMessage中

	public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

		final HttpHeaders headers = outputMessage.getHeaders();
//設置contenttype到HttpOutputMessage if (headers.getContentType() == null) { MediaType contentTypeToUse = contentType; if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { contentTypeToUse = getDefaultContentType(t); } if (contentTypeToUse != null) { headers.setContentType(contentTypeToUse); } } if (headers.getContentLength() == -1) { Long contentLength = getContentLength(t, headers.getContentType()); if (contentLength != null) { headers.setContentLength(contentLength); } }           /* 省略了不相干代碼 */ }

最終的Headers設置在ServletServerHttpResponse類中完成,

	private void writeHeaders() {
		if (!this.headersWritten) {
			for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
				String headerName = entry.getKey();
				for (String headerValue : entry.getValue()) {
//將復合類中之前設置的header(content-type)內容補充到servletResponse this.servletResponse.addHeader(headerName, headerValue); } } // HttpServletResponse exposes some headers as properties: we should include those if not already present if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) { this.servletResponse.setContentType(this.headers.getContentType().toString()); } if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null && this.headers.getContentType().getCharSet() != null) { this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name()); } this.headersWritten = true; } }

從上述的代碼中,我們可以看到在RequestResponseBodyMethodProcessor這個ReturnValueHandler中,media-type被單獨的邏輯進行處理,因此直接在ServletResponse中設置content-type header並不能正常生效。

需要在@RequestMapping中添加produces = {} 進行設置才可以。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM