近期作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 }
至此,控制層接口返回的數據,通過切面處理後,寫入輸出流中,返回給前端。