SpringCloud请求响应数据转换(一)

2021-06-16 13:05

阅读:691

标签:obj   produces   ack   linked   not   elf   resolve   分享图片   ppi   

异常现象

近期做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> 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")
 12     protected 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)
 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 requestedMediaTypes = getAcceptableMediaTypes(request);
 34 //获取控制层指定的返回媒体类型,默认为*/*,如@RequestMapping(value = "/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE),表示服务响应的格式为application/json格式。
 35         List 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 compatibleMediaTypes = new LinkedHashSet();
 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 mediaTypes = new ArrayList(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 causeRef = new AtomicReference();
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  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) advice).beforeBodyWrite((T) body, returnType,
19                         contentType, converterType, request, response);
20             }
21         }
22         return body;
23     }
24     @SuppressWarnings("unchecked")
25     private  List getMatchingAdvice(MethodParameter parameter, Class extends A> adviceType) {
26          //获取所有切面
27         List availableAdvice = getAdvice(adviceType);
28         if (CollectionUtils.isEmpty(availableAdvice)) {
29             return Collections.emptyList();
30         }
31         List result = new ArrayList(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{
 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      * 

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() ) { 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     }

 至此,控制层接口返回的数据,经过切面处理后,写入输出流中,返回给前端。

 返回数据处理过程涉及的类

技术分享图片

 

SpringCloud请求响应数据转换(一)

标签:obj   produces   ack   linked   not   elf   resolve   分享图片   ppi   

原文地址:https://www.cnblogs.com/shuimuzhushui/p/9724583.html


评论


亲,登录后才可以留言!