写在前面

在开发中,经常会有一个自定义输出数据格式的场景,此时如果用到 ResponseBodyAdvice 做全局的数据格式控制,在响应纯字符串数据时可能会遇到某些奇怪的问题,本文描述了两个和 String 相关的问题的解决方案,内容可能和网上的其他有些不同,问题较简单,纯记录贴。

  1. XXXX cannot be cast to java.lang.String 问题;
  2. 响应的字符串携带双引号问题。

本文 SpringBoot 环境:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>
<!-- SpringCloud项目 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>Hoxton.SR8</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

本文场景:普通的 Controller 使用 ResponseBodyAdvice 进行全局格式控制,微服务间相互调用的部分接口没有进行格式控制,否则调用完还需要再进行一次格式解析,较麻烦。

1. 问题解决

先提供问题解决方案,后续再描述原理,ResponseBodyAdvice 类如下这么写即可(不需要调整消息转换器)。

package com.nineya.user.handler;

import com.alibaba.fastjson.JSONObject;
import com.nineya.tool.restful.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * @author 殇雪话诀别
 * 2020/12/7
 */
@ControllerAdvice("com.nineya.user.controller.api")
public class ResponseResultHandler implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass,
                                  ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        serverHttpResponse.getHeaders().setContentType(MediaType.parseMediaType(MediaType.APPLICATION_JSON_VALUE));
        ResponseResult result = ResponseResult.access(o);
        if (o == null || o instanceof String) {
            return JSONObject.toJSONString(result);
        }
        return result;
    }
}

2. 问题解决步骤

  1. 首先遇到的是 .ResponseResult cannot be cast to java.lang.String 字符串类型转换问题,查看源码发现了如下这么一串代码,主要关注 3 点注释。
Object body;
Class<?> valueType;
Type targetType;
// 1. 此处判断 body 的数据类型
if (value instanceof CharSequence) {
	body = value.toString();
	valueType = String.class;
	targetType = String.class;
}
else {
	body = value;
	valueType = getReturnValueType(body, returnType);
	targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
省略亿点代码......
if (selectedMediaType != null) {
	selectedMediaType = selectedMediaType.removeQualityValue();
	for (HttpMessageConverter<?> converter : this.messageConverters) {
		GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
				(GenericHttpMessageConverter<?>) converter : null);
		if (genericConverter != null ?
				((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
				converter.canWrite(valueType, selectedMediaType)) {
            // 2. 此处调用 ResponseBodyAdvice 的代码进行格式控制
			body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
					(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
					inputMessage, outputMessage);
			if (body != null) {
				Object theBody = body;
				LogFormatUtils.traceDebug(logger, traceOn ->
						"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
				addContentDispositionHeader(inputMessage, outputMessage);
                // 3. 此处进行消息转换
				if (genericConverter != null) {
					genericConverter.write(body, targetType, selectedMediaType, outputMessage);
				}
				else {
					((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
				}
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("Nothing to write: null body");
				}
			}
			return;
		}
	}
}

可以到在使用 ResponseBodyAdvice 进行格式控制之前已经获取了返回值类型 valueType,默认的消息转换器列表 String 转换器在 Json 转换器前面,所以此处会被 String 转换器处理,泛型将会把 body 强转为 String。如果在进行格式转换时 String 被更换为其他对象,就会引发强转失败报错。

  1. 网上的教程只看到了一个版本,就是将 MappingJackson2HttpMessageConverter 顺序移到第一个,让其在 String 之前进行处理,由于该处理器也支持处理 String,所以 String 处理器就不会再进行处理,从而不会抛出异常。

    但是这个做法是存在问题的,当有部分接口没有进行格式处理、同时返回的是纯 String 时,响应结果将会多出两个双引号(如下),这是由于被 Json 转换器处理带来的结果,要解决这个问题,就不能将 Json 处理器放在最前面。

    "c6c8020a9220421593c4d7042611168e"
    
  2. 要解决强转问题 String 转换器必须在后面,要解决双引号问题 Sting 又必须在前面,貌似有点矛盾。但是明白了原由想要解决就简单了,首先为了解决没有进行格式处理、同时返回的是纯 String 的部分接口的双引号问题,不能调整处理器顺序。而强转 String 失败问题要解决则非常容易,在 ResponseBodyAdvice 中判断 body 的数据类型是不是 String,如果是字符串类型,我们手动转成 String 在返回即可。

    所以只需要以下这三行代码即可解决这两个问题:

         if (o == null || o instanceof String) {
             return JSONObject.toJSONString(result);
         }
    

    如果传入值原本就不是 String 响应数据也不能是 String 哦,否则 Json 转换器将会为其加上双引号。