异常现象
近期做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 }
至此,控制层接口返回的数据,经过切面处理后,写入输出流中,返回给前端。