RestTemplate
在运行一段时间后居然报空指针异常,可以根据StackTrace定位到是有一个HttpMessageConverter
为空;Content-type
为text/pain
,但返回结果总是被双引号包围,导致接口调用方解析失败,可以定位到是由于一个自定义的HttpMessageConverter
错误的拦截了text/plain
的produce
类型,并在字符串两端加上了双引号。HttpMessageConverter
可以把不同类型的body转为Java对象,也可以吧Java对象转为满足要求的body,在序列化与反序列化中有非常重要的作用。
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
,就是用这个HttpMessageConverter
的read()
方法读取参数,一旦处理完数据即将返回,又用同样的方法遍历HttpMessageConverter
列表,找出第一个canWrite()
返回true的HttpMessageConverter
,调用其write()
方法返回给客户端。ApplicationContext
refresh期间,HttpMessageConverter
开始初始化,初始化分别在三个类中进行:其中RequestMappingHandlerAdapter
和HttpMessageConvertersAutoConfiguration
是同时行的互不干扰,HttpMessageConverters
的初始化需要在前两个类初始化完成后才能进行;RequestMappingHandlerAdapter
初始化默认HttpMessageConverter
列表
WebMvcConfigurer
类型的自定义配置类(@Configuration
)的configureMessageConverters
方法初始化 HttpMessageConverter
列表;WebMvcConfigurer
配置,调用addDefaultHttpMessageConverters
方法初始化HttpMessageConverter
列表,默认HttpMessageConverter
列表都是根据ClassLoader
中是否加载否一个特定类来判断某一个HttpMessageConverter
是否需要加到默认列表中,并且在最后做了一下排序,仅仅是把xml类型的转换器放到了目前的HttpMessageConverter
列表最后;WebMvcConfigurer
类型的自定义配置类的extendMessageConverters
方法扩展HttpMessageConverter
列表,直接加在列表尾部HttpMessageConvertersAutoConfiguration
类将所有HttpMessageConverter
类型的组件(@Conponent/@Bean
等),初始化到一个有上下文提供的 HttpMessageConverter
列表中;HttpMessageConverters
初始化时,将2和3两个列表合并,如果上下文提供的和默认列表中有重复但对象并非是同一个,会把上下文提供的HttpMessageConverter
和默认列表中的HttpMessageConverter
放在相邻的位置,并且会把上下文提供的放在前面;把所有上下文提供的且不在默认列表中的HttpMessageConverter
放在整个合并列表的最前面,上下文提供的HttpMessageConverter
顺序由类上的@Order(value=1)
注解指定,value值越小越靠前,优先级越高。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);
}
}
自定义一个AHttpMessageConverter
,canRead()
和canWrite()
犯法都直接返回true
,并且把@Order
值设置为1
,因为使用Component
注解的上下文HttpMessageConverter
列表回被添加在HttpMessageConverter
前面,而spring
默认实现的HttpMessageConverter
order值都是Integer.MAX_VALUE
,因此AHttpMessageConverter
会匹配所有Content-Type
和Accept
。
类中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
如果修改JSONObject
为String
:
@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"
}
说明正是Accept
和Content-Type
都使用了AHttpMessageConverter
来序列化和反序列化。
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
只是一个简单的UnmodifiableList
不支持高并发,这样要么会导致List
无限膨胀,要么会导致空指针,因为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;
}
}
一个接口返回的Content-type
为text/pain
,但返回结果总是被双引号包围,导致接口调用方解析失败,可以定位到是由于一个自定义的HttpMessageConverter
错误的拦截了text/plain
的produce
类型,并在字符串两端加上了双引号。
理解上述原理后,应该可以猜想到应该是由错误的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
的锅,果不其然,FastJsonHttpMessageConverter
的canRead()
方法和canWrite()
方法都是是根据List
来判断的:只要是List
定义的MediaType
,都由FastJsonHttpMessageConverterEx
来序列化和反序列化,而我们自定的FastJsonHttpMessageConverterEx
加上了supportedMediaTypes.add(MediaType.TEXT_PLAIN);
,不出错就奇怪了。
删掉该行,问题解决,并且FastJsonHttpMessageConverterEx
该不该处理这么多类型还需要多考量,虽然目前系统运作良好,但是不保证新业务增加后会出现问题,因为目前我们只有applicaition/json;plain/text;application/object-stream
等几种类型,假设哪天出现一个image/jpeg
,保不齐又会出错,因为可能Fastjson
处理不了但又强行让其处理,也可能强行处理了,结果格式不对等。