SpringCloud請求響應數據轉換(一)

異常現象

近期作Spring Cloud項目,工程中對Controller添加ResponseBodyAdvice切面,在切片中將返回的結果封裝到ResultMessage(自定義結構),但在Controller的方法返回值爲字符串,客戶端支持的類型爲application/json時,出現如下異常:前端

  java.lang.ClassCastException: com.service.view.ResultMessage cannot be cast to java.lang.Stringjava

即沒法將ResultMessage對象轉換爲String。調試發現,當返回的是String字符串類型時,則會調StringHttpMessageConverter 將數據寫入響應流,添加響應頭等信息。web

在獲取接口數據與寫入響應流之間,會將切面處理後的ResultMessage對象交由StringHttpMessageConverter 寫入響應流,出現將ResultMessage賦值給一個String對象,從而致使類型轉換異常。spring

響應數據處理流程

大體流程(簡化請求端)以下:json

源碼分析

工程中自定義ResponseBodyAdvice切面時,對聲明@RestController註解的控制層接口,在返回數據的時候會對數據進行轉換,轉換過程當中會調自定義切面對數據處理。具體進行什麼轉換,會以客戶端支持的類型(如application/json或text/plain等)以及控制層返回數據的類型爲依據。Spring底層包含幾種轉換器,以下:瀏覽器

MVC中,從控制層返回數據到寫入響應流,須要經過RequestResponseBodyMethodProcessor類的handleReturnValue方法進行處理,其中會調AbstractMessageConverterMethodProcessor類中方法writeWithMessageConverters,經過消息轉換器將數據寫入響應流,包含3個關鍵步驟:數據結構

(1)轉換器的肯定,該類包含屬性List<HttpMessageConverter<?>> messageConverters,其中包含支持的全部轉換器,如上圖。從前日後依次遍歷全部轉換器,直到找到支持返回數據類型或媒體類型的轉換器。app

(2)切面數據處理,調自定義ResponseBodyAdvice切面(若是存在的話),對返回數據進行處理eclipse

(3)寫入響應流,經過消息轉換器將數據ServletServerHttpResponse。ide

關鍵方法爲writeWithMessageConverters:

  1 /**
  2      * Writes the given return type to the given output message.
  3      * @param value the value to write to the output message
  4      * @param returnType the type of the value
  5      * @param inputMessage the input messages. Used to inspect the {@code Accept} header.
  6      * @param outputMessage the output message to write to
  7      * @throws IOException thrown in case of I/O errors
  8      * @throws HttpMediaTypeNotAcceptableException thrown when the conditions indicated by {@code Accept} header on
  9      * the request cannot be met by the message converters
 10      */
 11     @SuppressWarnings("unchecked")
 12     protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
 13             ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
 14             throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
 15 
 16         Object outputValue;
 17         Class<?> valueType;
 18         Type declaredType;
 19 //判斷控制層返回的value類型,對String進行特殊處理,其餘獲取對應類型valueType(如java.util.ArrayList)和聲明類型declaredType(列表元素具體類型,如java.util.List<com.service.entity.PersonVO>)
 20         if (value instanceof CharSequence) {
 21             outputValue = value.toString();
 22             valueType = String.class;
 23             declaredType = String.class;
 24         }
 25         else {
 26             outputValue = value;
 27             valueType = getReturnValueType(outputValue, returnType);
 28             declaredType = getGenericType(returnType);
 29         }
 30 
 31         HttpServletRequest request = inputMessage.getServletRequest();
 32 //獲取瀏覽器支持的媒體類型,如*/*
 33         List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
 34 //獲取控制層指定的返回媒體類型,默認爲*/*,如@RequestMapping(value = "/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE),表示服務響應的格式爲application/json格式。
 35         List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
 36 
 37         if (outputValue != null && producibleMediaTypes.isEmpty()) {
 38             throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
 39         }
 40 //判斷瀏覽器支持的媒體類型是否兼容返回媒體類型
 41         Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
 42         for (MediaType requestedType : requestedMediaTypes) {
 43             for (MediaType producibleType : producibleMediaTypes) {
 44                 if (requestedType.isCompatibleWith(producibleType)) {
 45                     compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
 46                 }
 47             }
 48         }
 49         if (compatibleMediaTypes.isEmpty()) {
 50             if (outputValue != null) {
 51                 throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
 52             }
 53             return;
 54         }
 55 
 56         List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
 57         MediaType.sortBySpecificityAndQuality(mediaTypes);
 58 
 59         MediaType selectedMediaType = null;
 60         for (MediaType mediaType : mediaTypes) {
 61             if (mediaType.isConcrete()) {
 62                 selectedMediaType = mediaType;
 63                 break;
 64             }
 65             else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
 66                 selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
 67                 break;
 68             }
 69         }
 70 
 71         if (selectedMediaType != null) {
 72             selectedMediaType = selectedMediaType.removeQualityValue();
 73 //遍歷全部Http消息轉換器,如上圖,(1)首先Byte和String等非GenericHttpMessageConverter轉換器;
(2)MappingJackson2HttpMessageConverter轉換器繼承GenericHttpMessageConverter,會將對象類型轉換爲json(採用com.fasterxml.jackson)
74 for (HttpMessageConverter<?> messageConverter : this.messageConverters) { 75 //判斷轉換器是否爲GenericHttpMessageConverter,其中canWrite()方法判斷是否能經過該轉換器將響應寫入響應流,見後續代碼 76 if (messageConverter instanceof GenericHttpMessageConverter) { 77 if (((GenericHttpMessageConverter) messageConverter).canWrite( 78 declaredType, valueType, selectedMediaType)) { 79 //獲取切片;調切片的beforeBodyWrite方法,處理控制層方法返回值,最終outputValue爲處理後的數據,如工程中返回的ResultMessage 80 outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 81 (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 82 inputMessage, outputMessage); 83 if (outputValue != null) { 84 addContentDispositionHeader(inputMessage, outputMessage); 85 //將處理後的數據寫入響應流,同時添加響應頭,並調該轉換器的寫入方法;如MappingJackson2HttpMessageConverter的writeInternal方法,會將數據寫入json中,具體見後續代碼 86 ((GenericHttpMessageConverter) messageConverter).write( 87 outputValue, declaredType, selectedMediaType, outputMessage); 88 if (logger.isDebugEnabled()) { 89 logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 90 "\" using [" + messageConverter + "]"); 91 } 92 } 93 return; 94 } 95 } 96 //處理Byte和String等類型的數據 97 else if (messageConverter.canWrite(valueType, selectedMediaType)) { 98 outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 99 (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 100 inputMessage, outputMessage); 101 if (outputValue != null) { 102 addContentDispositionHeader(inputMessage, outputMessage); 103 ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage); 104 if (logger.isDebugEnabled()) { 105 logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 106 "\" using [" + messageConverter + "]"); 107 } 108 } 109 return; 110 } 111 } 112 } 113 114 if (outputValue != null) { 115 throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); 116 } 117 }

(1)肯定消息轉換器 

canWrite()方法判斷是否能經過該轉換器將響應寫入響應流,以控制層返回一個自定義對象爲例,會調AbstractJackson2HttpMessageConverter,即將數據已json格式返回到前端,其代碼以下:

 1 @Override
 2     public boolean canWrite(Class<?> clazz, MediaType mediaType) {
 3         //判斷客戶端是否支持返回的媒體類型
 4         if (!canWrite(mediaType)) {
 5             return false;
 6         }
 7         if (!logger.isWarnEnabled()) {
 8             return this.objectMapper.canSerialize(clazz);
 9         }
10         AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
11         //判斷是否能夠經過ObjectMapper對clazz進行序列化
12         if (this.objectMapper.canSerialize(clazz, causeRef)) {
13             return true;
14         }
15         logWarningIfNecessary(clazz, causeRef.get());
16         return false;
17     }

 其中方法參數,clazz爲上文中的valueType,即控制層返回數據類型;mediaType爲要寫入響應流的媒體類型,能夠爲null,典型值爲請求頭Accept(the media type to write, can be null if not specified. Typically the value of an Accept header.)。

對String或Byte等類型,在對應的轉換器中都重寫canWrite方法,以StringHttpMessageConverter爲例,代碼以下:

1 @Override
2     public boolean supports(Class<?> clazz) {
3         return String.class == clazz;
4     }

 (2)切面數據處理

beforeBodyWrite:RequestResponseBodyAdviceChain類的beforeBodyWrite方法,會獲取到ResponseBodyAdvice子類對應的切面,並調support方法判斷是否能夠處理某類型數據,調beforeBodyWrite方法進行數據處理

 1 @Override
 2     public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType,
 3             Class<? extends HttpMessageConverter<?>> converterType,
 4             ServerHttpRequest request, ServerHttpResponse response) {
 5 
 6         return processBody(body, returnType, contentType, converterType, request, response);
 7     }
 8 
 9     @SuppressWarnings("unchecked")
10     private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType,
11             Class<? extends HttpMessageConverter<?>> converterType,
12             ServerHttpRequest request, ServerHttpResponse response) {
13          //獲取並遍歷全部與ResponseBodyAdvice匹配的切面,其中returnType包含了請求方法相關信息
14         for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
15              //調切面的supports方法,判斷切面是否支持返回類型和轉換類型
16             if (advice.supports(returnType, converterType)) {
17                  //調切面的beforeBodyWrite方法,進行數據處理
18                 body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
19                         contentType, converterType, request, response);
20             }
21         }
22         return body;
23     }
24     @SuppressWarnings("unchecked")
25     private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
26          //獲取全部切面
27         List<Object> availableAdvice = getAdvice(adviceType);
28         if (CollectionUtils.isEmpty(availableAdvice)) {
29             return Collections.emptyList();
30         }
31         List<A> result = new ArrayList<A>(availableAdvice.size());
32         //遍歷全部切面,找到符合adviceType的切面
33         for (Object advice : availableAdvice) {
34             if (advice instanceof ControllerAdviceBean) {
35                 ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
36                 if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
37                     continue;
38                 }
39                 advice = adviceBean.resolveBean();
40             }
41              //判斷adviceType 是否爲advice.getClass()的父類或父接口等
42             if (adviceType.isAssignableFrom(advice.getClass())) {
43                 result.add((A) advice);
44             }
45         }
46         return result;
47     }

 第16和18行會調自定義ResponseBodyAdvice切面對應的方法,以下,其中還包含對異常狀況的處理。

 1 @RestControllerAdvice(annotations = RestController.class)
 2 public class ControllerInterceptor implements ResponseBodyAdvice<Object>{
 3     //異常狀況處理
 4     @ExceptionHandler(value = BizException.class)
 5     public String defaultErrorHandler(HttpServletRequest req, BizException e) throws Exception {
 6         ResultMessage rm = new ResultMessage();
 7         ErrorMessage errorMessage = new ErrorMessage(e.getErrCode(), e.getErrMsg());
 8         rm.setErrorMessage(errorMessage);
 9         rm.setSuccess(false);
10         return JSONUtil.ObjectToString(rm);
11     }
12 
13     //數據處理
14     @Override
15     public Object beforeBodyWrite(Object object, MethodParameter methodPram, MediaType mediaType,
16             Class<? extends HttpMessageConverter<?>> clazz, ServerHttpRequest request, ServerHttpResponse response) {
17         ResultMessage rm = new ResultMessage();
18         rm.setSuccess(true);
19         rm.setData(object);
20         
21         Object obj;
22          //處理控制層返回字符串狀況,解決上文說的類型轉換異常
23         if(object != null && object.getClass().equals(String.class)){
24             obj = JSONObject.fromObject(rm).toString();
25         }else{
26             obj = rm;
27         }
28         return obj;
29     }
30 
31     //肯定是否支持,此處返回true
32     @Override
33     public boolean supports(MethodParameter methodPram, Class<? extends HttpMessageConverter<?>> clazz) {
34         return true;
35     }
36 }

   其中,第23行是對控制層返回值爲字符串狀況的處理,防止出現類型轉換異常。

另外,@RestControllerAdvice支持@ControllerAdvice and @ResponseBody,即爲控制層的切面,doc的介紹以下:

  A convenience annotation that is itself annotated with @ControllerAdvice and @ResponseBody.

  Types that carry this annotation are treated as controller advice where @ExceptionHandler methods assume @ResponseBody semantics by default.

(3)寫入響應流

write方法會將(2)中處理後的數據寫入響應流,對String或Byte等類型,會調HttpMessageConverter的write方法;對對象等類型會調GenericHttpMessageConverter的write方法。

對象類型時,會調GenericHttpMessageConverter父類AbstractGenericHttpMessageConverter的write方法,以下:

 1 /**
 2      * This implementation sets the default headers by calling {@link #addDefaultHeaders},
 3      * and then calls {@link #writeInternal}.
 4      */
 5     public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
 6             throws IOException, HttpMessageNotWritableException {
 7 
 8         final HttpHeaders headers = outputMessage.getHeaders();
 9         //添加默認的響應頭,包括Content-Type和Content-Length
10         addDefaultHeaders(headers, t, contentType);
11 
12         if (outputMessage instanceof StreamingHttpOutputMessage) {
13             StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
14             streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
15                 @Override
16                 public void writeTo(final OutputStream outputStream) throws IOException {
17                     writeInternal(t, type, new HttpOutputMessage() {
18                         @Override
19                         public OutputStream getBody() throws IOException {
20                             return outputStream;
21                         }
22                         @Override
23                         public HttpHeaders getHeaders() {
24                             return headers;
25                         }
26                     });
27                 }
28             });
29         }
30         else {
31             //非StreamingHttpOutputMessage狀況下,會調該方法將數據寫入響應流
32             writeInternal(t, type, outputMessage);
33             outputMessage.getBody().flush();
34         }
35     }
36 /**
37      * Add default headers to the output message.
38      * <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content
39      * type was not provided, set if necessary the default character set, calls
40      * {@link #getContentLength}, and sets the corresponding headers.
41      * @since 4.2
42      */
43     protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
44          //設置Content-Type
45         if (headers.getContentType() == null) {
46             MediaType contentTypeToUse = contentType;
47             if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
48                 contentTypeToUse = getDefaultContentType(t);
49             }
50             else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
51                 MediaType mediaType = getDefaultContentType(t);
52                 contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
53             }
54             if (contentTypeToUse != null) {
55                 if (contentTypeToUse.getCharset() == null) {
56                     Charset defaultCharset = getDefaultCharset();
57                     if (defaultCharset != null) {
58                         contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
59                     }
60                 }
61                 headers.setContentType(contentTypeToUse);
62             }
63         }
64         //設置Content-Length,當t爲ArrayList對象時,值爲null
65         if (headers.getContentLength() < 0) {
66             Long contentLength = getContentLength(t, headers.getContentType());
67             if (contentLength != null) {
68                 headers.setContentLength(contentLength);
69             }
70         }
71     }

 第32行會調AbstractJackson2HttpMessageConverter的writeInternal方法。object爲經切面處理後的數據,經過com.fasterxml.jackson.databind.ObjectMapper寫入json。

 1 @Override
 2     protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
 3             throws IOException, HttpMessageNotWritableException {
 4 
 5         JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
 6         JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
 7         try {
 8             writePrefix(generator, object);
 9 
10             Class<?> serializationView = null;
11             FilterProvider filters = null;
12             Object value = object;
13             JavaType javaType = null;
14             if (object instanceof MappingJacksonValue) {
15                 MappingJacksonValue container = (MappingJacksonValue) object;
16                 value = container.getValue();
17                 serializationView = container.getSerializationView();
18                 filters = container.getFilters();
19             }
20             if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
21                 javaType = getJavaType(type, null);
22             }
23             ObjectWriter objectWriter;
24             if (serializationView != null) {
25                 objectWriter = this.objectMapper.writerWithView(serializationView);
26             }
27             else if (filters != null) {
28                 objectWriter = this.objectMapper.writer(filters);
29             }
30             else {
31                 objectWriter = this.objectMapper.writer();
32             }
33             if (javaType != null && javaType.isContainerType()) {
34                 objectWriter = objectWriter.forType(javaType);
35             }
36              //經過ObjectWrite構建json數據結構
37             objectWriter.writeValue(generator, value);
38 
39             writeSuffix(generator, object);
40             generator.flush();
41 
42         }
43         catch (JsonProcessingException ex) {
44             throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
45         }
46     }

 String或Byte等類型時,會調HttpMessageConverter的父類AbstractHttpMessageConverter的write方法,代碼與上文相似,只是getContentLength和writeInternal方法不一樣。以String爲例,會調StringHttpMessageConverter的writeInternal方法,代碼以下:

 1 //返回字符串對應的字節數長度,做爲Content-Length,上文中的異常就出如今此處。
 2 @Override
 3     protected Long getContentLength(String str, MediaType contentType) {
 4         Charset charset = getContentTypeCharset(contentType);
 5         try {
 6             return (long) str.getBytes(charset.name()).length;
 7         }
 8         catch (UnsupportedEncodingException ex) {
 9             // should not occur
10             throw new IllegalStateException(ex);
11         }
12     }
13 
14 @Override
15     protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
16         if (this.writeAcceptCharset) {
17             outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
18         }
19         Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
20         //將字符串數據copy後寫入輸出流
21         StreamUtils.copy(str, charset, outputMessage.getBody());
22     }
23 StreamUtils類:
24 /**
25      * Copy the contents of the given String to the given output OutputStream.
26      * Leaves the stream open when done.
27      * @param in the String to copy from
28      * @param charset the Charset
29      * @param out the OutputStream to copy to
30      * @throws IOException in case of I/O errors
31      */
32     public static void copy(String in, Charset charset, OutputStream out) throws IOException {
33         Assert.notNull(in, "No input String specified");
34         Assert.notNull(charset, "No charset specified");
35         Assert.notNull(out, "No OutputStream specified");
36         Writer writer = new OutputStreamWriter(out, charset);
37         writer.write(in);
38         writer.flush();
39     }

 至此,控制層接口返回的數據,通過切面處理後,寫入輸出流中,返回給前端。

 返回數據處理過程涉及的類

相關文章
相關標籤/搜索