• 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 将数据写入响应流,添加响应头等信息。

    在获取接口数据与写入响应流之间,会将切面处理后的ResultMessage对象交由StringHttpMessageConverter 写入响应流,出现将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")
     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     }

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

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

  • 相关阅读:
    学渣逆袭回忆录:0序
    selenium和Firefox版本不兼容
    学习Python的第一课(简单的单元测试)
    C# 实现模拟登录功能,实现公共类分享。
    MVC之权限管理-网站开发之路
    程序员学习之路
    【数据结构】浅谈线性数据结构
    【数据结构】二叉堆
    tire 学习心得
    洛谷P1801 黑匣子
  • 原文地址:https://www.cnblogs.com/shuimuzhushui/p/9724583.html
Copyright © 2020-2023  润新知