一、背景
最近做接口的性能改造,大概背景如下:
旧:
1.前端每秒轮询后端接口,接口返回数据状态,前端用状态做判断,变更页面交互。
2.前端固定调用后端接口,接口阻塞100秒,等待后端随时返回结果,100秒到达后无结果,直接失败。
新:
改为ServerSentEvent以text-event-stream固定时间窗口由后端返回处理进度。
二、简单对比
1.后端服务压力大。尤其在用户量大,并发量大,又是系统核心业务时,不好把控。
2.阻塞Servlet线程, 当前业务同时访问量大时,影响其他业务请求;最大等待时间不好把控,且受HTTP响应超时时间影响。
3.自由简单,且实时性比较强,只要不是高并发入口请求业务就能使用。不占用Servlet线程。
三、问题现象
1.主代码展示
以下代码均为精简后的最小问题复现demo
- SSE主接口
2.问题表现
通过curl加-N参数调用后,发现返回的data都被双引号包裹,并且前端的EventSource.onmessage无法回调到(总是触发onerror回调)
四.问题思考
参考并阅读以下文档
前端MDN ServerSentEvent文档
Spring ServerSentEvent文档
在前端侧浏览器看到开发者工具内EventStream标签页显示空白, 意识到确实是后端自身问题,观察数据结构后发现,像是返回了JSON结构的String字符串,只有JSON结构下String两侧才会有引号,接着立刻翻源码验证。
从源码org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler.HttpMessageConvertingHandler#sendInternal中找到线索,发现Spring在处理text-event-stream响应时,仍然是从Servlet Web的全局HttpMessageConverter查找,但这里的mediaType值到底是哪个值呢,存在以下三种情况
Controller方法整体的MediaType,这个值固定是text/event-streamSseEmitter#send(java.lang.Object, org.springframework.http.MediaType)方法整体的MediaType,这个值固定是text/plainSseEmitter#send(java.lang.Object, org.springframework.http.MediaType)方法第二个参数传入的MediaType,这个值动态的,我这里是application/json;charset=UTF-8
这里大概率是第二种导致的。
五、得到结论
由于在K8s内网部署,本机无法启动,关联的中间件和附加Bean太多。到这里为止,其实问题清晰了,结合对Spring源码的理解,项目中的全局HttpMessageConverter配置肯定有问题,我们公司前身有很多认为自己很厉害的人,都是从阿里转过来的,酷爱fastjson,项目基建时,肯定干掉了所有的HttpMessageConverter,统一换成了fastjson,接着找这个代码
这是2021年,前人留下来的宝藏,只能说这个人复制的时候可能没过脑
- 红色框出来的就是根因
- 绿色框出来的就是完全无脑,二进制文档,图片,也要Json吗?
1.看看上游Spring的处理
随即问题来了,Spring默认会有一大堆的HttpMessageConverter自动配置,当前这个是add到List<HttpMessageConverter<?>> converters的第一位吗?默认的还在不在?
源码如下: org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getMessageConverters
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHttpMessageConverters如下
如图就是Spring的解法
六、圈定影响
那么新的问题来了,如果这个项目中有其他直接返回String的接口,岂不是意味着全部被包了一层双引号?
恰好想起之前有其他人不听劝,不使用Spring Actuator的健康检查,非要手搓,返回的恰好是String,线上验证一下
那就是影响到了所有直接返回String的接口。
七、问题复现
本地不能启动,最小demo,debug一下
debug启动后发现确实是这里影响的,这里会走两次
一次前面的text/plain
实际的第二个参数的MediaType,值application/json;charset=UTF-8
看看HttpMessageConverter是不是错选了
根因确定: text/plain错误配置了使用FastJsonHttpMessageConverter,导致返回的不是字符串,而是json字符串
八、问题修复
修复就非常简单了,删除不合理的MediaType配置,最前面追加StringHttpMessageConverter即可
当然为了缩小影响范围,只删除MediaType.TEXT_PLAIN配置即可至此,问题修复。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。