异常现象
近期做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