阐述问题最近看了Spring MVC源码,感觉特别有趣,像发现了新大陆一般,不能自拔。
最近发现一个关于FastJsonHttpMessageConverter特别有趣的一个点,它默认的supportMediaType竟然是MediaType.ALL。
public FastJsonHttpMessageConverter() {
super(MediaType.ALL);
}
说明FastJsonHttpMessageConverter不配置supportMediaType,那么默认是MediaType.ALL。
FastJsonHttpMessageConverter应该显式配置其支持的消息格式是MediaType.APPLICATION_JSON_UTF8
//使用阿里 FastJson 作为JSON MessageConverter
@Override
public void configureMessageConverters(List> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
log.info("converters:" + converters.toString());
List supportMediaTypeList = new ArrayList<>();
// supportMediaTypeList.add(MediaType.TEXT_HTML);
supportMediaTypeList.add(MediaType.APPLICATION_JSON_UTF8);
// supportMediaTypeList.add(MediaType.IMAGE_GIF);
// supportMediaTypeList.add(MediaType.IMAGE_JPEG);
// supportMediaTypeList.add(MediaType.IMAGE_PNG);
FastJsonConfig config = new FastJsonConfig();
// config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setSerializerFeatures(
SerializerFeature.WriteMapNullValue,//保留空的字段
SerializerFeature.WriteNullStringAsEmpty,//String null -> ""
SerializerFeature.WriteNullNumberAsZero,//Number null -> 0
SerializerFeature.WriteNullListAsEmpty,//List null-> []
SerializerFeature.WriteNullBooleanAsFalse);//Boolean null -> false
converter.setFastJsonConfig(config);
converter.setSupportedMediaTypes(supportMediaTypeList);
converter.setDefaultCharset(Charset.forName("UTF-8"));
converters.add(converter);
}
我有一个业务场景,需要缓存商品详情页,手动渲染Thymeleaft模板,在Controller中返回html片段。
配置如下:
@ResponseBody
public String list(MiaoshaUser miaoshaUser) throws IOException {
modelMap.addAttribute("user", miaoshaUser);
//取缓存
String htmlCached = redisService.get(GoodsKey.getGoodsList, "", String.class);
if (!StringUtils.isEmpty(htmlCached)) {
return htmlCached;
}
List goodsVoList = goodsService.listGoodsVo();
modelMap.addAttribute("goodsList", goodsVoList);
SpringWebContext springWebContext = new SpringWebContext(request, response, request.getServletContext(),
request.getLocale(), modelMap, applicationContext);
String html = thymeleafViewResolver.getTemplateEngine().process("goods_list", springWebContext);
if (!StringUtils.isEmpty(html)) {
redisService.set(GoodsKey.getGoodsList, "", html);
}
return html;
}
可是最终渲染的界面惨不忍睹,界面出现了大量的nt这样的字符
如果我不用FastJsonHttpMessageConverter呢,界面却显示很正常,这是为什么呢。
虽然在Controller中可以通过手动打开输出流,设置ContentType,把Thymeleaf模板输出到Response的body,可以解决问题。
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setStatus(200);
try {
response.getWriter().write(html);
} catch (IOException ex) {
log.error(ex.getMessage());
}
但是本着阿Q精神,我Debug消息转换器的流程,发现问题出在FastJsonHttpMessageConverter没有配置MediaType.APPLICATION_JSON_UTF8,
导致其默认的MediaType.ALL影响Thymeleaf模板最后的输出。
Debug之路
RequestResponseBodyProcessor实现了HandlerMethodReturnValueHandler、HandlerMethodAgumentResolver接口,实现了boolean supportsParamter()、void resolveArgument()、boolean supportsReturnType()、void handleReturnValue()
所以具备了处理参数、处理返回值的能力。官方解释.RequestResponseBodyProcessor能够解析用@RequestBody注解的参数和通过使用HttpMessageConverter读取并写入请求体或响应来处理用@ResponseBody注解的方法的返回值。
public interface HandlerMethodReturnValueHandler {
boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {}
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {}
handleReturnValue()获取inputMessage,outMessage对象,调用writeWithMessageConverters()方法,让消息转换器对消息进行下一步处理.
public interface HttpMessage {
HttpHeaders getHeaders();
}
public interface HttpOutputMessage extends HttpMessage {
OutputStream getBody() throws IOException;
}
public interface HttpInputMessage extends HttpMessage {
InputStream getBody() throws IOException;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
在AbstarctMessageConverterProcessor的writeWithMessageConverters()中,
根据HandlerMethod中的ReturnValueMethodParameter对象获valueType(返回值类型),同时也能得到outputValue(返回值)、decalredType(返回值实际类型)
Object outputValue;
Class> valueType;
Type declaredType;
if (value instanceof CharSequence) {
outputValue = value.toString();
valueType = String.class;
declaredType = String.class;
}
else {
outputValue = value;
valueType = getReturnValueType(outputValue, returnType);
declaredType = getGenericType(returnType);
}
根据request对象中获取请求中Accept的属性值,得requestedMediaTypes
ListrequestedMediaTypes = getAcceptableMediaTypes(request); private List getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { List mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); return (mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes); } @Override public List resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { for (ContentNegotiationStrategy strategy : this.strategies) { List mediaTypes = strategy.resolveMediaTypes(request); if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) { continue; } return mediaTypes; } return Collections.emptyList(); }
根据HandlerMapping中的produces属性获得producibleMediaTypes。
如果没有显式设置produces属性,我们只能通过遍历所有的HttpMessageConverter,通过canWrite()方法找到支持解析Java对象的HttpMessageConverter,并且把其所支持的mediaType加入mediaTypes集合里面。
ListproducibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
protected ListgetProducibleMediaTypes(HttpServletRequest request, Class> valueClass, 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()) { List result = new ArrayList (); for (HttpMessageConverter> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && declaredType != null) { if (((GenericHttpMessageConverter>) converter).canWrite(declaredType, valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } else if (converter.canWrite(valueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } return result; } else { return Collections.singletonList(MediaType.ALL); } }
通过两次for循环,比较requestedMediaTypes和producibleMediaTypes
得到compatiableMediaTypes。如果compatiableMediaTypes为空,会抛出HttpMediaTypeNotAcceptableException异常。白话意思就是producibleMediaTypes没有一个MediaType与requestedMediaTypes匹配,肯定无法执行下一步了。
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
Set compatibleMediaTypes = new linkedHashSet();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
排序、for循环compatiableMediaTypes,通过isConcrete()判断消息格式是否具体(类型和子类型是否为通配符*),得到selectedMediaType(最终的MediaType)
ListmediaTypes = new ArrayList (compatibleMediaTypes); MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null; for (MediaType mediaType : mediaTypes) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } }
for循环已配置所有的HttpMessageConverter,调用canWrite()方法,根据valueType(返回值类型)和selectedMediaType来判断消息是否可以转换。
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as "" + selectedMediaType +
"" using [" + messageConverter + "]");
}
}
return;
}
}
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as "" + selectedMediaType +
"" using [" + messageConverter + "]");
}
}
return;
}
}
}
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
如果没有配置MediaType.APPLICATION_JSON_UTF8,默认值是MediaType.ALL,FastJsonHttpMessageConverter会去处理消息格式为"text/html;charset=UTF-8",会执行这一段代码
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as "" + selectedMediaType +
"" using [" + messageConverter + "]");
}
}
return;
}
}
接着调用FastJsonHttpMessageConverter的write()方法写入消息
public void write(Object o, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
super.write(o, contentType, outputMessage);// support StreamingHttpOutputMessage in spring4.0+
//writeInternal(o, outputMessage);
}
接着调用AbstaractHttpMessageConverter中的write()写入响应头和消息内容
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
(StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
接着调用FastJsonHttpMessageConverter的writeInternal()进行消息转换
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
ByteArrayOutputStream outnew = new ByteArrayOutputStream();
try {
HttpHeaders headers = outputMessage.getHeaders();
//获取全局配置的filter
SerializeFilter[] globalFilters = fastJsonConfig.getSerializeFilters();
List allFilters = new ArrayList(Arrays.asList(globalFilters));
boolean isJsonp = false;
//不知道为什么会有这行代码, 但是为了保持和原来的行为一致,还是保留下来
Object value = strangeCodeForJackson(object);
if (value instanceof FastJsonContainer) {
FastJsonContainer fastJsonContainer = (FastJsonContainer) value;
PropertyPreFilters filters = fastJsonContainer.getFilters();
allFilters.addAll(filters.getFilters());
value = fastJsonContainer.getValue();
}
//revise 2017-10-23 ,
// 保持原有的MappingFastJsonValue对象的contentType不做修改 保持旧版兼容。
// 但是新的JSONPObject将返回标准的contentType:application/javascript ,不对是否有function进行判断
if (value instanceof MappingFastJsonValue) {
if(!StringUtils.isEmpty(((MappingFastJsonValue) value).getJsonpFunction())){
isJsonp = true;
}
} else if (value instanceof JSONPObject) {
isJsonp = true;
}
int len = JSON.writeJSonString(outnew, //
fastJsonConfig.getCharset(), //
value, //
fastJsonConfig.getSerializeConfig(), //
//fastJsonConfig.getSerializeFilters(), //
allFilters.toArray(new SerializeFilter[allFilters.size()]),
fastJsonConfig.getDateFormat(), //
JSON.DEFAULT_GENERATE_FEATURE, //
fastJsonConfig.getSerializerFeatures());
if (isJsonp) {
headers.setContentType(APPLICATION_JAVAscript);
}
if (fastJsonConfig.isWriteContentLength()) {
headers.setContentLength(len);
}
outnew.writeTo(outputMessage.getBody());
} catch (JSonException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
} finally {
outnew.close();
}
}
我们可以观察到这个方法中有一个JSON.writeJSonString()方法,我点进去看一看
public static final int writeJSonString(OutputStream os, //
Charset charset, //
Object object, //
SerializeConfig config, //
SerializeFilter[] filters, //
String dateFormat, //
int defaultFeatures, //
SerializerFeature... features) throws IOException {
SerializeWriter writer = new SerializeWriter(null, defaultFeatures, features);
try {
JSonSerializer serializer = new JSonSerializer(writer, config);
if (dateFormat != null && dateFormat.length() != 0) {
serializer.setDateFormat(dateFormat);
serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
}
if (filters != null) {
for (SerializeFilter filter : filters) {
serializer.addFilter(filter);
}
}
serializer.write(object);
int len = writer.writeToEx(os, charset);
return len;
} finally {
writer.close();
}
}
一看吓一跳。卧槽,把html序列化了,GG。终于找到问题的始作俑者了。
如果配置了MediaType.APPLICATION_JSON_UTF8,FastJsonHttpMessageConverter
只能处理"application/json;charset=UTF-8"的消息,"text/html;charset=UTF-8"格式的消息被StringHttpMessageConverter得到了处理,会执行这一段代码
else if (messageConverter.canWrite(valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
(Class extends HttpMessageConverter>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as "" + selectedMediaType +
"" using [" + messageConverter + "]");
}
}
return;
}
接着调用AbstaractHttpMessageConverter中的write()写入响应头和消息内容
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
(StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
最终调用StringHttpMessageConverter的writeInternal写入消息到outputMessage.getBody()中,输出html片段。
@Override
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
if (this.writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy(str, charset, outputMessage.getBody());
}
关于HttpMessageConverter加载顺序,可以在WebMvcConfigurationSupport看到端倪。
protected final List> getMessageConverters() { if (this.messageConverters == null) { this.messageConverters = new ArrayList >(); configureMessageConverters(this.messageConverters); if (this.messageConverters.isEmpty()) { addDefaultHttpMessageConverters(this.messageConverters); } extendMessageConverters(this.messageConverters); } return this.messageConverters; } protected final void addDefaultHttpMessageConverters(List > messageConverters) { StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); stringConverter.setWriteAcceptCharset(false); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(stringConverter); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new SourceHttpMessageConverter ()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { messageConverters.add(new MappingJackson2XmlHttpMessageConverter( Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build())); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { messageConverters.add(new MappingJackson2HttpMessageConverter( Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build())); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } }
我们其实是重写configureMessageConverters()方法去配置FastJsonHttpMessageConverter,所以它是第一个。
尾言
等休息的时候,再写Spring MVC源码分析请求响应流程,源码分析RequestMappingHandlerMapping和RequestMappingHandlerAdapter。



