栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

你真的会用HttpMessageConverter吗?

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

你真的会用HttpMessageConverter吗?

前言

最近看了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

    List requestedMediaTypes = 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集合里面。

List producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
    protected List getProducibleMediaTypes(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)

 List mediaTypes = 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>) 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>) 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>) 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>) 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。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/235626.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号