HttpMessageConverter使用不当导致的问题及其原理、配置

15 篇文章 0 订阅
1 篇文章 0 订阅

两个问题

  • spring boot RestTemplate在运行一段时间后居然报空指针异常,可以根据StackTrace定位到是有一个HttpMessageConverter为空;
  • 一个接口返回的Content-typetext/pain,但返回结果总是被双引号包围,导致接口调用方解析失败,可以定位到是由于一个自定义的HttpMessageConverter错误的拦截了text/plainproduce类型,并在字符串两端加上了双引号。

HttpMessageConverter作用

HttpMessageConverter可以把不同类型的body转为Java对象,也可以吧Java对象转为满足要求的body,在序列化与反序列化中有非常重要的作用。

HttpMessageConverter匹配规则

  • Http请求头中会包含Accept,告知服务器要传回什么样的数据,如Accept: application/json, text/javascript, */*; q=0.01;同时会指定Content-Type告知服务器本次body传输的参数数据是什么类型,服务器可以根据数据类型转化为服务器内部对象并提取参数;
  • Server端接收到请求,会判断自己是否支持客户端传过来的参数类型(MediaType),如果没有任何一个支持,服务器将返回406(HttpMediaTypeNotSupportedException);
  • Server处理完参数和逻辑,准备根据客户端要求的Accept返回,这时服务端又会判断自己是否支持返回这种类型(MediaType),如果不支持,服务器将返回406(HttpMediaTypeNotAcceptableException);
  • HttpMessageConverter的作用就是服务器判断自己是否支持某种MediaType
  • HttpMessageConverter不是仅仅只有一个而是一个列表,通过责任链的方式匹配:循序遍历所有HttpMessageConverter,调用其canRead()方法,若返回true表示可以处理,一旦有某个HttpMessageConverter可以处理某一请求的参数MediaType,就是用这个HttpMessageConverterread()方法读取参数,一旦处理完数据即将返回,又用同样的方法遍历HttpMessageConverter列表,找出第一个canWrite()返回true的HttpMessageConverter,调用其write()方法返回给客户端。

HttpMessageConverter初始化时序图

在这里插入图片描述

  1. ApplicationContextrefresh期间,HttpMessageConverter开始初始化,初始化分别在三个类中进行:其中RequestMappingHandlerAdapterHttpMessageConvertersAutoConfiguration是同时行的互不干扰,HttpMessageConverters的初始化需要在前两个类初始化完成后才能进行;
  2. RequestMappingHandlerAdapter初始化默认HttpMessageConverter列表
    • 调用所有WebMvcConfigurer类型的自定义配置类(@Configuration)的configureMessageConverters方法初始化 HttpMessageConverter列表;
    • 如果没有自定义的WebMvcConfigurer配置,调用addDefaultHttpMessageConverters方法初始化HttpMessageConverter列表,默认HttpMessageConverter列表都是根据ClassLoader中是否加载否一个特定类来判断某一个HttpMessageConverter是否需要加到默认列表中,并且在最后做了一下排序,仅仅是把xml类型的转换器放到了目前的HttpMessageConverter列表最后;
    • 调用所有WebMvcConfigurer类型的自定义配置类的extendMessageConverters方法扩展HttpMessageConverter列表,直接加在列表尾部
  3. HttpMessageConvertersAutoConfiguration类将所有HttpMessageConverter类型的组件(@Conponent/@Bean等),初始化到一个有上下文提供的 HttpMessageConverter列表中;
  4. HttpMessageConverters初始化时,将2和3两个列表合并,如果上下文提供的和默认列表中有重复但对象并非是同一个,会把上下文提供的HttpMessageConverter和默认列表中的HttpMessageConverter放在相邻的位置,并且会把上下文提供的放在前面;把所有上下文提供的且不在默认列表中的HttpMessageConverter放在整个合并列表的最前面,上下文提供的HttpMessageConverter顺序由类上的@Order(value=1)注解指定,value值越小越靠前,优先级越高。

自定义HttpMessageConverter

package com.enmo.dbaas.common.config.feigninterceptor;

import org.springframework.core.annotation.Order;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import javax.activation.UnsupportedDataTypeException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * Created by IntelliJ IDEA
 *
 * @author chenlei
 * @date 2020/1/11
 * @time 17:44
 * @desc AHttpMessageConverter
 */
@Component
@Order(1)
public class AHttpMessageConverter extends AbstractHttpMessageConverter<Object> implements GenericHttpMessageConverter<Object> {
    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        outputMessage.getBody().write("{}".getBytes());
        outputMessage.getBody().flush();
    }

    @Override
    public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
        return true;
    }

    @Override
    public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        InputStream in = inputMessage.getBody();
        if (String.class.getTypeName().equals(type.getTypeName())) {
            byte[] bytes = new byte[65536];
            int offset = 0;

            while(true) {
                int readCount = in.read(bytes, offset, bytes.length - offset);
                if(readCount == -1) {
                    return new String(bytes, "UTF-8");
                }

                offset += readCount;
                if(offset == bytes.length) {
                    byte[] newBytes = new byte[bytes.length * 3 / 2];
                    System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
                    bytes = newBytes;
                }
            }
        }
        throw new UnsupportedDataTypeException(type.getClass().getTypeName());
    }

    @Override
    public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
        return true;
    }

    @Override
    public void write(Object o, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        super.write(o, contentType, outputMessage);
    }
}

自定义一个AHttpMessageConvertercanRead()canWrite()犯法都直接返回true,并且把@Order值设置为1,因为使用Component注解的上下文HttpMessageConverter列表回被添加在HttpMessageConverter前面,而spring默认实现的HttpMessageConverterorder值都是Integer.MAX_VALUE,因此AHttpMessageConverter会匹配所有Content-TypeAccept

类中read()方法只支持String类型的参数,其他类型的参数一律抛出UnsupportedDataTypeException
类中write()方法均返回字符串"{}"

假设在Controller中写一个这样的方法:

@PostMapping("install/invoke/test")
public ResultData invokeTest(@RequestHeader String a,
                         @RequestParam String b,
                         @RequestParam String c,
                         @RequestBody JSONObject d) {
    log.info("{}, {}, {} ,{}", a, b, c, d);
    return new JSONObject().fluentPut("key", "value");
}

将报错:

org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is javax.activation.UnsupportedDataTypeException: java.lang.Class
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:216)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:157)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:130)
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter.doFilterInternal(HttpTraceFilter.java:88)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:114)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:104)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:745)
Caused by: javax.activation.UnsupportedDataTypeException: java.lang.Class
	at com.enmo.dbaas.common.config.feigninterceptor.AHttpMessageConverter.read(AHttpMessageConverter.java:72)
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:204)
	... 62 common frames omitted

如果修改JSONObjectString

@PostMapping("install/invoke/test")
public ResultData invokeTest(@RequestHeader String a,
                         @RequestParam String b,
                         @RequestParam String c,
                         @RequestBody String d) {
    log.info("{}, {}, {} ,{}", a, b, c, d);
    return new JSONObject().fluentPut("key", "value");
}

将返回:

{}

而不是:

{
	"key": "value"
}

说明正是AcceptContent-Type都使用了AHttpMessageConverter来序列化和反序列化。

解决问题

RestTemplate NPE

spring boot RestTemplate在运行一段时间后居然报空指针异常,可以根据StackTrace定位到是有一个HttpMessageConverter为空

过一段时间才为空,肯定是运行过程中有地方把HttpMessageConverter列表某个索引的值改为了null,不是一启动就报错,也不是调用特定接口报错,而是某一次调用出现NPE后,之后的任意一次请求全部报NPE。

恰好找到了这篇文章:处理restTemplate的messageConverters设置StringHttpMessageConverter

文中提到每次调用RestTemplate的时候手动设置字符集为UTF-8,线上偶发NPE,这与我们的设置极为相似:

restTemplate.getMessageConverters()
        .add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));

我们知道HttpMessageConverters类中的List<HttpMessageConverter>只是一个简单的UnmodifiableList<HttpMessageConverter<?>>不支持高并发,这样要么会导致List<HttpMessageConverter>无限膨胀,要么会导致空指针,因为List.add(int index, Object o)原理是先把列表index之后的所有元素后移一位,然后再把索引为index的位置赋值。spring的组件默认是单例的,在高并发的情况下,所有线程同时修改一个不支持高并发的列表某,一个线程刚好把所有元素后移一位u,第一个元素还没来得及赋值,可能恰好另外一个线程已经开始遍历,取到第一个HttpMessageConverter恰好是null,于是开始报空指针,这是偶发原因;

那为什么可能出现一次后,就有可能之后所有的请求全部报空指针呢,特别是在凌晨有多个定时任务同时跑的情况下,出现“永久NPE”的概率极大。这是因为刚好有两个线程同时设置索引为0的HttpMessageConverter,其中一个线程刚刚把所有元素后移一位,还没来得及给索引为0的位置赋值,另一个线程又开始把所有元素后移,导致索引为1的元素永久为空,再也救不回来了。

找的文章里的解决方案和单例模式类似,但也并不能完全避免高并发问题,还是需要加同步块并且使用duoble check,这种方式非常不优雅,因为spring 的bean默认是单例,我们完全可以在初始化的时候配置好,后面直接注入使用即可,不需要二次配置:

@Configuration
public class HttpConfiguration {
    @Bean
    RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
        messageConverters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }
}

text/pain有引号

一个接口返回的Content-typetext/pain,但返回结果总是被双引号包围,导致接口调用方解析失败,可以定位到是由于一个自定义的HttpMessageConverter错误的拦截了text/plainproduce类型,并在字符串两端加上了双引号。

理解上述原理后,应该可以猜想到应该是由错误的HttpMessageConverter序列化导致的,在不配置任何自定义HttpMessageConverter的时候一切正常,我们只加了一个自定义的FastJsonHttpMessageConverter

package com.enmo.dbaas.common.config;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class FastJsonHttpMessageConverterEx extends FastJsonHttpMessageConverter {
    public FastJsonHttpMessageConverterEx() {
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");    // 自定义时间格式
        fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue); // 正常转换 null 值
        List<MediaType> supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
        supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
        supportedMediaTypes.add(MediaType.APPLICATION_PDF);
        supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XML);
        supportedMediaTypes.add(MediaType.IMAGE_GIF);
        supportedMediaTypes.add(MediaType.IMAGE_JPEG);
        supportedMediaTypes.add(MediaType.IMAGE_PNG);
        supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
        supportedMediaTypes.add(MediaType.TEXT_HTML);
        supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
        supportedMediaTypes.add(MediaType.TEXT_XML);
        supportedMediaTypes.add(MediaType.TEXT_PLAIN);
        this.setSupportedMediaTypes(supportedMediaTypes);
        this.setFastJsonConfig(fastJsonConfig);
    }
}

这是上下文提供的,会放在HttpMessageConverter列表的第一个,优先级最高。
结合Fastjson序列化字符串的时候会加上引号:
在这里插入图片描述
因此肯定是这个FastJsonHttpMessageConverterEx的锅,果不其然,FastJsonHttpMessageConvertercanRead()方法和canWrite()方法都是是根据List<MediaType> supportedMediaTypes来判断的:只要是List<MediaType> supportedMediaTypes定义的MediaType,都由FastJsonHttpMessageConverterEx来序列化和反序列化,而我们自定的FastJsonHttpMessageConverterEx加上了supportedMediaTypes.add(MediaType.TEXT_PLAIN);,不出错就奇怪了。

删掉该行,问题解决,并且FastJsonHttpMessageConverterEx该不该处理这么多类型还需要多考量,虽然目前系统运作良好,但是不保证新业务增加后会出现问题,因为目前我们只有applicaition/json;plain/text;application/object-stream等几种类型,假设哪天出现一个image/jpeg,保不齐又会出错,因为可能Fastjson处理不了但又强行让其处理,也可能强行处理了,结果格式不对等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值