先上代码:
代码1:
@Controller
public class TestController {
@RequestMapping(value = "/a")
@ResponseBody
public Object a() throws Exception {
MyResponse myResponse = new MyResponse();
myResponse.code = 200;
myResponse.msg = "hello world, 你好世界。";
return myResponse;
}
@RequestMapping(value = "/b")
@ResponseBody
public String b() throws Exception {
return "hello world, 你好世界。";
}
static class MyResponse {
public int code;
public String msg;
public T data;
}
}
代码2:
~ curl -X GET "http://localhost:8080/a"
{"code":200,"msg":"hello world, 你好世界。","data":null}
~ curl -X GET "http://localhost:8080/b"
hello world, ?????
我们使用spring mvc开发restful接口的一般模式如代码1第4第11行代码所示,使用@ResponseBody注解一个方法,表示把该方法的返回值写进response的body里。同时声明一个对象,形如MyResponse,作为controller方法的返回值,统一restful接口的结构。代码2第1第2行调用该接口,正常返回中文字符,不乱码。
如果我们的返回值不需要最外层的结构,返回给用户的就是我我们处理后的一个字符串,把这个字符串存放在response的body里返回,那么这个方法顺理成章的被写成代码1第13第17行的样子。代码2第3第4行是调用这个接口的过程,会出现中文乱码。
面对乱码,最直接的解决办法就是查看方法的返回值是如何被写到response的body里的,这就是渲染的过程,我们知道,spring mvc有两个地方会渲染,上面的情况会在 ServletInvocableHandlerMethod 类里进行渲染,可以参考spring源码解析-web系列(四):九大组件之HandlerAdapter文章里 ServletInvocableHandlerMethod 部分,spring源码解析-web系列(四):九大组件之HandlerAdapter文章代码8第20~第21行执行渲染过程,渲染的代码如下:
代码3 (org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite):
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
代码3第5行获取handler,代码3第9行调用handler的handleReturnValue方法渲染返回值。对于我们的这个方法,获取到的handler为RequestResponseBodyMethodProcessor类型。
代码4 (org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue):
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = this.createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = this.createOutputMessage(webRequest);
this.writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
RequestResponseBodyMethodProcessor的handleReturnValue方法如代码4所示,它调用了父类的writeWithMessageConverters方法完成渲染。
代码5 (org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters):
protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object outputValue;
Class valueType;
Object declaredType;
if (value instanceof CharSequence) {
outputValue = value.toString();
valueType = String.class;
declaredType = String.class;
} else {
outputValue = value;
valueType = this.getReturnValueType(value, returnType);
declaredType = this.getGenericType(returnType);
}
if (this.isResourceType(value, returnType)) {
outputMessage.getHeaders().set("Accept-Ranges", "bytes");
if (value != null && inputMessage.getHeaders().getFirst("Range") != null) {
Resource resource = (Resource)value;
try {
List httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
outputValue = HttpRange.toResourceRegions(httpRanges, resource);
valueType = outputValue.getClass();
declaredType = RESOURCE_REGION_LIST_TYPE;
} catch (IllegalArgumentException var17) {
outputMessage.getHeaders().set("Content-Range", "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
MediaType contentType = outputMessage.getHeaders().getContentType();
Object mediaTypesToUse;
if (contentType != null && contentType.isConcrete()) {
mediaTypesToUse = Collections.singletonList(contentType);
} else {
HttpServletRequest request = inputMessage.getServletRequest();
List requestedMediaTypes = this.getAcceptableMediaTypes(request);
List producibleMediaTypes = this.getProducibleMediaTypes(request, valueType, (Type)declaredType);
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
mediaTypesToUse = new ArrayList();
Iterator var13 = requestedMediaTypes.iterator();
while(var13.hasNext()) {
MediaType requestedType = (MediaType)var13.next();
Iterator var15 = producibleMediaTypes.iterator();
while(var15.hasNext()) {
MediaType producibleType = (MediaType)var15.next();
if (requestedType.isCompatibleWith(producibleType)) {
((List)mediaTypesToUse).add(this.getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (((List)mediaTypesToUse).isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality((List)mediaTypesToUse);
}
MediaType selectedMediaType = null;
Iterator var21 = ((List)mediaTypesToUse).iterator();
while(var21.hasNext()) {
MediaType mediaType = (MediaType)var21.next();
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
HttpMessageConverter converter;
GenericHttpMessageConverter genericConverter;
label138: {
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
var21 = this.messageConverters.iterator();
while(var21.hasNext()) {
converter = (HttpMessageConverter)var21.next();
genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
if (genericConverter != null) {
if (((GenericHttpMessageConverter)converter).canWrite((Type)declaredType, valueType, selectedMediaType)) {
break label138;
}
} else if (converter.canWrite(valueType, selectedMediaType)) {
break label138;
}
}
}
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
return;
}
outputValue = this.getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, converter.getClass(), inputMessage, outputMessage);
if (outputValue != null) {
this.addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(outputValue, (Type)declaredType, selectedMediaType, outputMessage);
} else {
converter.write(outputValue, selectedMediaType, outputMessage);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + converter + "]");
}
}
}
代码5有点长,我先补充一点前置知识:HandlerMethodReturnValueHandler(我们用的是RequestResponseBodyMethodProcessor实现类)使用来处理(渲染)我们的controller方法的返回值的,但是它也不是直接渲染的,而是借助 HttpMessageConverter 的概念, HttpMessageConverter 的作用是:从request读取数据;把数据写入response。为了灵活通用可扩展, HttpMessageConverter 也是按照spring的套路实现的,每个 HttpMessageConverter 可以判断自己是否支持某种类型的处理,在调用它的地方有若干个 HttpMessageConverter ,循环这些 HttpMessageConverter ,找到支持的Converter来处理。 HttpMessageConverter 接口定义如下:
代码6 (org.springframework.http.converter.HttpMessageConverter):
public interface HttpMessageConverter {
boolean canRead(Class> var1, @Nullable MediaType var2);
boolean canWrite(Class> var1, @Nullable MediaType var2);
List getSupportedMediaTypes();
T read(Class extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
通过代码6可以发现: HttpMessageConverter 在判断是否可以渲染时需要对象的类型和MediaType两个信息,对象的类型就是我们的controller方法返回的对象类型,MediaType就是response里的content-type。HttpMessageConverter 的 getSupportedMediaTypes 方法返回这个 HttpMessageConverter 对应的可以写入的content-type的值。
代码5第2行~第85行获取 selectedMediaType ,selectedMediaType就是写进response的content-type。代码2第87第112行获取HttpMessageConverter。代码5第114第126行使用HttpMessageConverter进行渲染。
这里最关键的就是如何获取 selectedMediaType :代码5第39行获取request里header里的 Accept 设置的值,这里取到的值为 * / * 。代码5第40行获取这个controller处理以后写入到response里的content-type的值。获取过程如下:
代码7 (org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.getProducibleMediaTypes):
protected List getProducibleMediaTypes(HttpServletRequest request, Class> valueClass, @Nullable Type declaredType) {
Set mediaTypes = (Set)request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList(mediaTypes);
} else if (this.allSupportedMediaTypes.isEmpty()) {
return Collections.singletonList(MediaType.ALL);
} else {
List result = new ArrayList();
Iterator var6 = this.messageConverters.iterator();
while(true) {
while(var6.hasNext()) {
HttpMessageConverter> converter = (HttpMessageConverter)var6.next();
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter)converter).canWrite(declaredType, valueClass, (MediaType)null)) {
result.addAll(converter.getSupportedMediaTypes());
}
} else if (converter.canWrite(valueClass, (MediaType)null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
}
}
代码7第2行,先获取request里的HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE属性,这个属性就是我们配置在@RequestMapping注解里的produces里的值,在HandlerMapping获取handler时被赋值到request里。如果有值,直接返回;否则代码7第8~第24行,循环所有的 HttpMessageConverter ,使用MediaType为null,判断HttpMessageConverter是否可以渲染这个对象,如果可以渲染,则把HttpMessageConverter支持的content-type返回。
代码5第68行对找到的MediaType进行排序,代码5第71~第85行,获取最合适的MediaType。
渲染的过程分析完了,那么乱码到底是怎么出现的呢?因为controller的返回值不同,导致找到的 HttpMessageConverter 不同,进而导致 MediaType 不同,所以前端看到的是乱码。
对于代码1第6行的a方法,由于@RequestMapping注解里produces属性为空且返回值为MyResponse类型,在获取producibleMediaTypes时, 支持的 HttpMessageConverter 有MappingJackson2HttpMessageConverter,获取到的producibleMediaTypes为 application/json 和 application/*+json 两个,选择到最优的MediaType为 application/json ,最后会被MappingJackson2HttpMessageConverter渲染,在MappingJackson2HttpMessageConverter中添加defaultCharset,由于MappingJackson2HttpMessageConverter中的defaultCharset为 UTF-8 ,最终content-type为 application/json;charset=UTF-8 。
对于代码1第15行的b方法,由于@RequestMapping注解里produces属性为空且返回值为String类型,在获取producibleMediaTypes时, 支持的 HttpMessageConverter 有 StringHttpMessageConverter 和 MappingJackson2HttpMessageConverter 两个,获取到的producibleMediaTypes为 text/plain 、 * / * 、 application/json 、 application/*+json 四个,选择到最优的MediaType为 text/plain ,最后会被StringHttpMessageConverter渲染,在StringHttpMessageConverter中添加defaultCharset,由于StringHttpMessageConverter中的defaultCharset为 ISO-8859-1 ,最终content-type为 text/plain;charset=ISO-8859-1 。
下面代码可以证明上面的分析:
代码8:
~ curl -i -X GET "http://localhost:8080/a"
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 12 Aug 2019 09:10:10 GMT
{"code":200,"msg":"hello world, 你好世界。","data":null}
~
~
~
~ curl -i -X GET "http://localhost:8080/b"
HTTP/1.1 200
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 18
Date: Mon, 12 Aug 2019 09:10:12 GMT
hello world, ?????
这个bug的原因就是方法返回值不同引起的MediaType不同,进而导致content-type不同。
建议我们在使用的时候指定@RequestMapping注解的produces属性。