SpringCloud請求響應資料轉換(一)
異常現象
近期做Spring Cloud專案,工程中對Controller新增ResponseBodyAdvice切面,在切片中將返回的結果封裝到ResultMessage(自定義結構),但在Controller的方法返回值為字串,客戶端支援的型別為application/json時,出現以下異常:
java.lang.ClassCastException: com.service.view.ResultMessage cannot be cast to java.lang.String
即無法將ResultMessage物件轉換為String。除錯發現,當返回的是String字串型別時,則會調StringHttpMessageConverter 將資料寫入響應流,會新增響應頭等資訊。其中計算響應資料長度Content-Length時,會將ResultMessage物件賦值給一個String物件,導致型別轉換異常。
響應資料處理流程
大致流程(簡化請求端)如下:
原始碼分析
工程中自定義ResponseBodyAdvice切面時,對宣告@RestController註解的控制層介面,在返回資料的時候會對資料進行轉換,轉換過程中會調自定義切面對資料處理。具體進行什麼轉換,會以客戶端支援的型別(如application/json或text/plain等)以及控制層返回資料的型別為依據。Spring底層包含幾種轉換器,如下:
MVC中,從控制層返回資料到寫入響應流,需要通過RequestResponseBodyMethodProcessor類的handleReturnValue方法進行處理,其中會調AbstractMessageConverterMethodProcessor類中方法writeWithMessageConverters,通過訊息轉換器將資料寫入響應流,包含3個關鍵步驟:
(1)轉換器的確定,該類包含屬性List<HttpMessageConverter<?>> messageConverters,其中包含支援的所有轉換器,如上圖。從前往後依次遍歷所有轉換器,直到找到支援返回資料型別或媒體型別的轉換器。
(2)切面資料處理,調自定義ResponseBodyAdvice切面(如果存在的話),對返回資料進行處理
(3)寫入響應流,通過訊息轉換器將資料ServletServerHttpResponse。
關鍵方法為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") 12protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, 13ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) 14throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { 15 16Object outputValue; 17Class<?> valueType; 18Type declaredType; 19 //判斷控制層返回的value型別,對String進行特殊處理,其他獲取對應型別valueType(如java.util.ArrayList)和宣告型別declaredType(列表元素具體型別,如java.util.List<com.service.entity.PersonVO>) 20if (value instanceof CharSequence) { 21outputValue = value.toString(); 22valueType = String.class; 23declaredType = String.class; 24} 25else { 26outputValue = value; 27valueType = getReturnValueType(outputValue, returnType); 28declaredType = getGenericType(returnType); 29} 30 31HttpServletRequest request = inputMessage.getServletRequest(); 32 //獲取瀏覽器支援的媒體型別,如*/* 33List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request); 34 //獲取控制層指定的返回媒體型別,預設為*/*,如@RequestMapping(value = "/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE),表示服務響應的格式為application/json格式。 35List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); 36 37if (outputValue != null && producibleMediaTypes.isEmpty()) { 38throw new IllegalArgumentException("No converter found for return value of type: " + valueType); 39} 40 //判斷瀏覽器支援的媒體型別是否相容返回媒體型別 41Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); 42for (MediaType requestedType : requestedMediaTypes) { 43for (MediaType producibleType : producibleMediaTypes) { 44if (requestedType.isCompatibleWith(producibleType)) { 45compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); 46} 47} 48} 49if (compatibleMediaTypes.isEmpty()) { 50if (outputValue != null) { 51throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); 52} 53return; 54} 55 56List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); 57MediaType.sortBySpecificityAndQuality(mediaTypes); 58 59MediaType selectedMediaType = null; 60for (MediaType mediaType : mediaTypes) { 61if (mediaType.isConcrete()) { 62selectedMediaType = mediaType; 63break; 64} 65else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { 66selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; 67break; 68} 69} 70 71if (selectedMediaType != null) { 72selectedMediaType = selectedMediaType.removeQualityValue(); 73 //遍歷所有Http訊息轉換器,如上圖,(1)首先Byte和String等非GenericHttpMessageConverter轉換器; (2)MappingJackson2HttpMessageConverter轉換器繼承GenericHttpMessageConverter,會將物件型別轉換為json(採用com.fasterxml.jackson) 74for (HttpMessageConverter<?> messageConverter : this.messageConverters) { 75 //判斷轉換器是否為GenericHttpMessageConverter,其中canWrite()方法判斷是否能通過該轉換器將響應寫入響應流,見後續程式碼 76if (messageConverter instanceof GenericHttpMessageConverter) { 77if (((GenericHttpMessageConverter) messageConverter).canWrite( 78declaredType, valueType, selectedMediaType)) { 79 //獲取切片;調切片的beforeBodyWrite方法,處理控制層方法返回值,最終outputValue為處理後的資料,如工程中返回的ResultMessage 80outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 81(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 82inputMessage, outputMessage); 83if (outputValue != null) { 84addContentDispositionHeader(inputMessage, outputMessage); 85 //將處理後的資料寫入響應流,同時新增響應頭,並調該轉換器的寫入方法;如MappingJackson2HttpMessageConverter的writeInternal方法,會將資料寫入json中,具體見後續程式碼 86((GenericHttpMessageConverter) messageConverter).write( 87outputValue, declaredType, selectedMediaType, outputMessage); 88if (logger.isDebugEnabled()) { 89logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 90"\" using [" + messageConverter + "]"); 91} 92} 93return; 94} 95} 96 //處理Byte和String等型別的資料 97else if (messageConverter.canWrite(valueType, selectedMediaType)) { 98outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, 99(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), 100inputMessage, outputMessage); 101if (outputValue != null) { 102addContentDispositionHeader(inputMessage, outputMessage); 103((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage); 104if (logger.isDebugEnabled()) { 105logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + 106"\" using [" + messageConverter + "]"); 107} 108} 109return; 110} 111} 112} 113 114if (outputValue != null) { 115throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); 116} 117}
(1)確定訊息轉換器
canWrite()方法判斷是否能通過該轉換器將響應寫入響應流,以控制層返回一個自定義物件為例,會調AbstractJackson2HttpMessageConverter,即將資料已json格式返回到前端,其程式碼如下:
1 @Override 2public boolean canWrite(Class<?> clazz, MediaType mediaType) { 3//判斷客戶端是否支援返回的媒體型別 4if (!canWrite(mediaType)) { 5return false; 6} 7if (!logger.isWarnEnabled()) { 8return this.objectMapper.canSerialize(clazz); 9} 10AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); 11//判斷是否可以通過ObjectMapper對clazz進行序列化 12if (this.objectMapper.canSerialize(clazz, causeRef)) { 13return true; 14} 15logWarningIfNecessary(clazz, causeRef.get()); 16return 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 2public boolean supports(Class<?> clazz) { 3return String.class == clazz; 4}
(2)切面資料處理
beforeBodyWrite:RequestResponseBodyAdviceChain類的beforeBodyWrite方法,會獲取到 ResponseBodyAdvice子類對應的切面,並調support方法判斷是否可以處理某型別資料,調beforeBodyWrite方法進行資料處理
1 @Override 2public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType, 3Class<? extends HttpMessageConverter<?>> converterType, 4ServerHttpRequest request, ServerHttpResponse response) { 5 6return processBody(body, returnType, contentType, converterType, request, response); 7} 8 9@SuppressWarnings("unchecked") 10private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType, 11Class<? extends HttpMessageConverter<?>> converterType, 12ServerHttpRequest request, ServerHttpResponse response) { 13//獲取並遍歷所有與ResponseBodyAdvice匹配的切面,其中returnType包含了請求方法相關資訊 14for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { 15//調切面的supports方法,判斷切面是否支援返回型別和轉換型別 16if (advice.supports(returnType, converterType)) { 17//調切面的beforeBodyWrite方法,進行資料處理 18body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType, 19contentType, converterType, request, response); 20} 21} 22return body; 23} 24@SuppressWarnings("unchecked") 25private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) { 26//獲取所有切面 27List<Object> availableAdvice = getAdvice(adviceType); 28if (CollectionUtils.isEmpty(availableAdvice)) { 29return Collections.emptyList(); 30} 31List<A> result = new ArrayList<A>(availableAdvice.size()); 32//遍歷所有切面,找到符合adviceType的切面 33for (Object advice : availableAdvice) { 34if (advice instanceof ControllerAdviceBean) { 35ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; 36if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { 37continue; 38} 39advice = adviceBean.resolveBean(); 40} 41//判斷adviceType 是否為advice.getClass()的父類或父介面等 42if (adviceType.isAssignableFrom(advice.getClass())) { 43result.add((A) advice); 44} 45} 46return result; 47}
第16和18行會調自定義 ResponseBodyAdvice切面對應的方法,如下,其中還包含對異常情況的處理。
1 @RestControllerAdvice(annotations = RestController.class) 2 public class ControllerInterceptor implements ResponseBodyAdvice<Object>{ 3//異常情況處理 4@ExceptionHandler(value = BizException.class) 5public String defaultErrorHandler(HttpServletRequest req, BizException e) throws Exception { 6ResultMessage rm = new ResultMessage(); 7ErrorMessage errorMessage = new ErrorMessage(e.getErrCode(), e.getErrMsg()); 8rm.setErrorMessage(errorMessage); 9rm.setSuccess(false); 10return JSONUtil.ObjectToString(rm); 11} 12 13//資料處理 14@Override 15public Object beforeBodyWrite(Object object, MethodParameter methodPram, MediaType mediaType, 16Class<? extends HttpMessageConverter<?>> clazz, ServerHttpRequest request, ServerHttpResponse response) { 17ResultMessage rm = new ResultMessage(); 18rm.setSuccess(true); 19rm.setData(object); 20 21Object obj; 22//處理控制層返回字串情況,解決上文說的型別轉換異常 23if(object != null && object.getClass().equals(String.class)){ 24obj = JSONObject.fromObject(rm).toString(); 25}else{ 26obj = rm; 27} 28return obj; 29} 30 31//確定是否支援,此處返回true 32@Override 33public boolean supports(MethodParameter methodPram, Class<? extends HttpMessageConverter<?>> clazz) { 34return 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*/ 5public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage) 6throws IOException, HttpMessageNotWritableException { 7 8final HttpHeaders headers = outputMessage.getHeaders(); 9//新增預設的響應頭,包括Content-Type和Content-Length 10addDefaultHeaders(headers, t, contentType); 11 12if (outputMessage instanceof StreamingHttpOutputMessage) { 13StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage; 14streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { 15@Override 16public void writeTo(final OutputStream outputStream) throws IOException { 17writeInternal(t, type, new HttpOutputMessage() { 18@Override 19public OutputStream getBody() throws IOException { 20return outputStream; 21} 22@Override 23public HttpHeaders getHeaders() { 24return headers; 25} 26}); 27} 28}); 29} 30else { 31//非StreamingHttpOutputMessage情況下,會調該方法將資料寫入響應流 32writeInternal(t, type, outputMessage); 33outputMessage.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*/ 43protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{ 44//設定Content-Type 45if (headers.getContentType() == null) { 46MediaType contentTypeToUse = contentType; 47if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { 48contentTypeToUse = getDefaultContentType(t); 49} 50else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { 51MediaType mediaType = getDefaultContentType(t); 52contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); 53} 54if (contentTypeToUse != null) { 55if (contentTypeToUse.getCharset() == null) { 56Charset defaultCharset = getDefaultCharset(); 57if (defaultCharset != null) { 58contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset); 59} 60} 61headers.setContentType(contentTypeToUse); 62} 63} 64//設定Content-Length,當t為ArrayList物件時,值為null 65if (headers.getContentLength() < 0) { 66Long contentLength = getContentLength(t, headers.getContentType()); 67if (contentLength != null) { 68headers.setContentLength(contentLength); 69} 70} 71}
第32行會調 AbstractJackson2HttpMessageConverter 的writeInternal方法。object為經切面處理後的資料,通過 com.fasterxml.jackson.databind.ObjectMapper 寫入json。
1 @Override 2protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) 3throws IOException, HttpMessageNotWritableException { 4 5JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); 6JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); 7try { 8writePrefix(generator, object); 9 10Class<?> serializationView = null; 11FilterProvider filters = null; 12Object value = object; 13JavaType javaType = null; 14if (object instanceof MappingJacksonValue) { 15MappingJacksonValue container = (MappingJacksonValue) object; 16value = container.getValue(); 17serializationView = container.getSerializationView(); 18filters = container.getFilters(); 19} 20if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { 21javaType = getJavaType(type, null); 22} 23ObjectWriter objectWriter; 24if (serializationView != null) { 25objectWriter = this.objectMapper.writerWithView(serializationView); 26} 27else if (filters != null) { 28objectWriter = this.objectMapper.writer(filters); 29} 30else { 31objectWriter = this.objectMapper.writer(); 32} 33if (javaType != null && javaType.isContainerType()) { 34objectWriter = objectWriter.forType(javaType); 35} 36//通過ObjectWrite構建json資料結構 37objectWriter.writeValue(generator, value); 38 39writeSuffix(generator, object); 40generator.flush(); 41 42} 43catch (JsonProcessingException ex) { 44throw 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 3protected Long getContentLength(String str, MediaType contentType) { 4Charset charset = getContentTypeCharset(contentType); 5try { 6return (long) str.getBytes(charset.name()).length; 7} 8catch (UnsupportedEncodingException ex) { 9// should not occur 10throw new IllegalStateException(ex); 11} 12} 13 14 @Override 15protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException { 16if (this.writeAcceptCharset) { 17outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); 18} 19Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); 20//將字串資料copy後寫入輸出流 21StreamUtils.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*/ 32public static void copy(String in, Charset charset, OutputStream out) throws IOException { 33Assert.notNull(in, "No input String specified"); 34Assert.notNull(charset, "No charset specified"); 35Assert.notNull(out, "No OutputStream specified"); 36Writer writer = new OutputStreamWriter(out, charset); 37writer.write(in); 38writer.flush(); 39}
至此,控制層介面返回的資料,經過切面處理後,寫入輸出流中,返回給前端。
返回資料處理過程涉及的類