废弃fastjson!大型项目迁移Gson保姆级实战
前言
大家好,又双叒叕见面了,我是天天放大家鸽子的蛮三刀。
在被大家取关之前,我立下一个远大的理想,一定要在这周更新文章。现在看来,flag有用了
本篇文章是我这一个多月来帮助组内废弃fastjson框架的总结,我们将大部分Java仓库从fastjson迁移至了Gson。
这么做的主要的原因是公司受够了fastjson频繁的安全漏洞问题,每一次出现漏洞都要推一次全公司的fastjson强制版本升级,很令公司头疼。
文章的前半部分,我会简单分析各种json解析框架的优劣,并给出企业级项目迁移json框架的几种解决方案。
在文章的后半部分,我会结合这一个月的经验,总结下Gson的使用问题,以及fastjson迁移到Gson踩过的深坑。
文章目录:
为何要放弃fastjson?
fastjson替代方案
三种json框架的特点
性能对比
最终选择方案
替换依赖时的注意事项
谨慎,谨慎,再谨慎
做好开发团队和测试团队的沟通
做好回归接口测试
考虑迁移前后的性能差异
使用Gson替换fastjson
Json反序列化
范型处理
ListMap写入
驼峰与下划线转换
迁移常见问题踩坑
Date序列化方式不同
SpringBoot异常
Swagger异常
MappingJsonObject作为入参异常
注意:是否使用fastjson是近年来一个争议性很大的话题,本文无意讨论框架选型的对错,只关注迁移这件事中遇到的问题进行反思和思考。大家如果有想发表的看法,可以在评论区理性讨论。本文阅读大概需要:5分钟
码字不易,欢迎关注我的个人公众号:后端技术漫谈(二维码见文章底部)
为何要放弃fastjson?
究其原因,是fastjson漏洞频发,导致了公司内部需要频繁的督促各业务线升级fastjson版本,来防止安全问题。
fastjson在2020年频繁暴露安全漏洞,此漏洞可以绕过autoType开关来实现反序列化远程代码执行并获取服务器访问权限。
从2019年7月份发布的v1。2。59一直到2020年6月份发布的v1。2。71,每个版本的升级中都有关于AutoType的升级,涉及13个正式版本。
fastjson中与AutoType相关的版本历史:1。2。59发布,增强AutoType打开时的安全性fastjson
1。2。60发布,增加了AutoType黑名单,修复拒绝服务安全问题fastjson
1。2。61发布,增加AutoType安全黑名单fastjson
1。2。62发布,增加AutoType黑名单、增强日期反序列化和JSONPathfastjson
1。2。66发布,Bug修复安全加固,并且做安全加固,补充了AutoType黑名单fastjson
1。2。67发布,Bug修复安全加固,补充了AutoType黑名单fastjson
1。2。68发布,支持GEOJSON,补充了AutoType黑名单
1。2。69发布,修复新发现高危AutoType开关绕过安全漏洞,补充了AutoType黑名单
1。2。70发布,提升兼容性,补充了AutoType黑名单
1。2。71发布,补充安全黑名单,无新增利用,预防性补充
相比之下,其他的json框架,如Gson和Jackson,漏洞数量少很多,高危漏洞也比较少,这是公司想要替换框架的主要原因。
fastjson替代方案
本文主要讨论Gson替换fastjson框架的实战问题,所以在这里不展开详细讨论各种json框架的优劣,只给出结论。
经过评估,主要有Jackson和Gson两种json框架放入考虑范围内,与fastjson进行对比。
三种json框架的特点FastJson速度快
fastjson相对其他JSON库的特点是快,从2011年fastjson发布1。1。x版本之后,其性能从未被其他Java实现的JSON库超越。
使用广泛
fastjson在阿里巴巴大规模使用,在数万台服务器上部署,fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。
测试完备
fastjson有非常多的testcase,在1。2。11版本中,testcase超过3321个。每次发布都会进行回归测试,保证质量稳定。
使用简单
fastjson的API十分简洁。
Jackson容易使用jacksonAPI提供了一个高层次外观,以简化常用的用例。
无需创建映射API提供了默认的映射大部分对象序列化。
性能高快速,低内存占用,适合大型对象图表或系统。
干净的JSONjackson创建一个干净和紧凑的JSON结果,这是让人很容易阅读。
不依赖库不需要任何其他的库,除了JDK。
Gson提供一种机制,使得将Java对象转换为JSON或相反如使用toString以及构造器(工厂方法)一样简单。
允许预先存在的不可变的对象转换为JSON或与之相反。
允许自定义对象的表现形式
支持任意复杂的对象
输出轻量易读的JSON
性能对比
同事撰写的性能对比源码:
https:github。comzysrxxjsoncomparison
本文不详细讨论性能的差异,毕竟这其中涉及了很多各个框架的实现思路和优化,所以只给出结论:1。序列化单对象性能FastjsonJacksonGson,其中Fastjson和Jackson性能差距很小,Gson性能较差
2。序列化大对象性能JacksonFastjsonGson,序列化大Json对象时JacksonGsonFastjson,Jackson序列化大数据时性能优势明显
3。反序列化单对象性能FastjsonJacksonGson,性能差距较小
4。反序列化大对象性能FastjsonJacksonGson,性能差距较很小
最终选择方案
Jackson适用于高性能场景,Gson适用于高安全性场景
对于新项目仓库,不再使用fastjson。对于存量系统,考虑到Json更换成本,由以下几种方案可选:
项目未使用autoType功能,建议直接切换为非fastjson,如果切换成本较大,可以考虑继续使用fastjson,关闭safemode。
业务使用了autoType功能,建议推进废弃fastjson。
替换依赖注意事项
企业项目或者说大型项目的特点:
代码结构复杂,团队多人维护。
承担重要线上业务,一旦出现严重bug会导致重大事故。
如果是老项目,可能缺少文档,不能随意修改,牵一发而动全身。
项目有很多开发分支,不断在迭代上线。
所以对于大型项目,想要做到将底层的fastjson迁移到gson是一件复杂且痛苦的事情,其实对于其他依赖的替换,也都一样。
我总结了如下几个在替换项目依赖过程中要特别重视的问题。
谨慎,谨慎,再谨慎
再怎么谨慎都不为过,如果你要更改的项目是非常重要的业务,那么一旦犯下错误,代价是非常大的。并且,对于业务方和产品团队来说,没有新的功能上线,但是系统却炸了,是一件无法忍受的事情。尽管你可能觉得很委屈,因为只有你或者你的团队知道,虽然业务看上去没变化,但是代码底层已经发生了翻天覆地的变化。
所以,谨慎点!
做好开发团队和测试团队的沟通
在依赖替换的过程中,需要做好项目的规划,比如分模块替换,严格细分排期。
把前期规划做好,开发和测试才能有条不紊的进行工作。
开发之间,需要提前沟通好开发注意事项,比如依赖版本问题,防止由多个开发同时修改代码,最后发现使用的版本不同,接口用法都不同这种很尴尬,并且要花额外时间处理的事情。
而对于测试,更要事先沟通好。一般来说,测试不会太在意这种对于业务没有变化的技术项目,因为既不是优化速度,也不是新功能。但其实迁移涉及到了底层,很容易就出现BUG。要让测试团队了解更换项目依赖,是需要大量的测试时间投入的,成本不亚于新功能,让他们尽量重视起来。
做好回归接口测试
上面说到测试团队需要投入大量工时,这些工时主要都用在项目功能的整体回归上,也就是回归测试。
当然,不只是业务回归测试,如果有条件的话,要做接口回归测试。
如果公司有接口管理平台,那么可以极大提高这种项目测试的效率。
打个比方,在一个模块修改完成后,在测试环境(或者沙箱环境),部署一个线上版本,部署一个修改后的版本,直接将接口返回数据进行对比。一般来说是Json对比,网上也有很多的Json对比工具:
https:www。sojson。com
考虑迁移前后的性能差异
正如上面描述的Gson和Fastjson性能对比,替换框架需要注意框架之间的性能差异,尤其是对于流量业务,也就是高并发项目,响应时间如果发生很大的变化会引起上下游的注意,导致一些额外的后果。
使用Gson替换Fastjson
这里总结了两种json框架常用的方法,贴出详细的代码示例,帮助大家快速的上手Gson,无缝切换!
Json反序列化StringjsonCase〔{id:10001,date:1609316794600,name:小明},{id:10002,date:1609316794600,name:小李}〕;
fastjson
JSONArrayjsonArrayJSON。parseArray(jsonCase);
System。out。println(jsonArray);
System。out。println(jsonArray。getJSONObject(0)。getString(name));
System。out。println(jsonArray。getJSONObject(1)。getString(name));
输出:
〔{date:1609316794600,name:小明,id:10001},{date:1609316794600,name:小李,id:10002}〕
小明
小李
Gson
JsonArrayjsonArrayGsongson。fromJson(jsonCase,JsonArray。class);
System。out。println(jsonArrayGson);
System。out。println(jsonArrayGson。get(0)。getAsJsonObject。get(name)。getAsString);
System。out。println(jsonArrayGson。get(1)。getAsJsonObject。get(name)。getAsString);
输出:
〔{id:10001,date:1609316794600,name:小明},{id:10002,date:1609316794600,name:小李}〕
小明
小李
看得出,两者区别主要在get各种类型上,Gson调用方法有所改变,但是变化不大。
那么,来看下空对象反序列化会不会出现异常:StringjsonObjectEmptyCase{};
fastjson
JSONObjectjsonObjectEmptyJSON。parseObject(jsonObjectEmptyCase);
System。out。println(jsonObjectEmpty);
System。out。println(jsonObjectEmpty。size);
输出:
{}
0
Gson
JsonObjectjsonObjectGsonEmptygson。fromJson(jsonObjectEmptyCase,JsonObject。class);
System。out。println(jsonObjectGsonEmpty);
System。out。println(jsonObjectGsonEmpty。size);
输出:
{}
0
没有异常,开心。
看看空数组呢,毕竟感觉比{}更加容易出错。StringjsonArrayEmptyCase;
fastjson
JSONArrayjsonArrayEmptyJSON。parseArray(jsonArrayEmptyCase);
System。out。println(jsonArrayEmpty);
System。out。println(jsonArrayEmpty。size);
输出:
0
Gson
JsonArrayjsonArrayGsonEmptygson。fromJson(jsonArrayEmptyCase,JsonArray。class);
System。out。println(jsonArrayGsonEmpty);
System。out。println(jsonArrayGsonEmpty。size);
输出:
0
两个框架也都没有问题,完美解析。
范型处理
解析泛型是一个非常常用的功能,我们项目中大部分fastjson代码就是在解析json和JavaBean。实体类
UserusernewUser;
user。setId(1L);
user。setUserName(马云);
fastjson
ListUseruserListResultFastjsonJSONArray。parseArray(JSON。toJSONString(userList),User。class);
ListUseruserListResultFastjson2JSON。parseObject(JSON。toJSONString(userList),newTypeReferenceListUser{});
System。out。println(userListResultFastjson);
System。out。println(userListResultFastjson2userListResultFastjson2);
输出:
userListResultFastjson〔User〔Hash483422889,id1,userName马云〕,〕
userListResultFastjson2〔User〔Hash488970385,id1,userName马云〕,〕
Gson
ListUseruserListResultTruegson。fromJson(gson。toJson(userList),newTypeTokenListUser{}。getType);
System。out。println(userListResultGsonuserListResultGson);
输出:
userListResultGson〔User〔Hash1435804085,id1,userName马云〕,〕
可以看出,Gson也能支持泛型。
ListMap写入
这一点fastjson和Gson有区别,Gson不支持直接将List写入value,而fastjson支持。
所以Gson只能将List解析后,写入value中,详见如下代码:实体类
UserusernewUser;
user。setId(1L);
user。setUserName(马云);
fastjson
JSONObjectjsonObject1newJSONObject;
jsonObject1。put(user,user);
jsonObject1。put(userList,userList);
System。out。println(jsonObject1);
输出:
{userList:〔{id:1,userName:马云},〕,user:{id:1,userName:马云}}
Gson
JsonObjectjsonObjectnewJsonObject;
jsonObject。add(user,gson。toJsonTree(user));
System。out。println(jsonObject);
输出:
{user:{id:1,userName:马云},userList:〔{id:1,userName:马云},〕}
如此一来,Gson看起来就没有fastjson方便,因为放入List是以gson。toJsonTree(user)的形式放入的。这样就不能先入对象,在后面修改该对象了。(有些同学比较习惯先放入对象,再修改对象,这样的代码就得改动)
驼峰与下划线转换
驼峰转换下划线依靠的是修改Gson的序列化模式,修改为LOWERCASEWITHUNDERSCORESGsonBuildergsonBuildernewGsonBuilder;
gsonBuilder。setFieldNamingPolicy(FieldNamingPolicy。LOWERCASEWITHUNDERSCORES);
GsongsonUnderScoregsonBuilder。create;
System。out。println(gsonUnderScore。toJson(user));
输出:
{id:1,username:马云}
常见问题排雷
下面整理了我们在公司项目迁移Gson过程中,踩过的坑,这些坑现在写起来感觉没什么技术含量。但是这才是我写这篇文章的初衷,帮助大家把这些很难发现的坑避开。
这些问题有的是在测试进行回归测试的时候发现的,有的是在自测的时候发现的,有的是在上线后发现的,比如Swagger挂了这种不会去测到的问题。
Date序列化方式不同
不知道大家想过一个问题没有,如果你的项目里有缓存系统,使用fastjson写入的缓存,在你切换Gson后,需要用Gson解析出来。所以就一定要保证两个框架解析逻辑是相同的,但是,显然这个愿望是美好的。
在测试过程中,发现了Date类型,在两个框架里解析是不同的方式。
fastjson:Date直接解析为Unix
Gson:直接序列化为标准格式Date
导致了Gson在反序列化这个json的时候,直接报错,无法转换为Date。
解决方案:
新建一个专门用于解析Date类型的类:importcom。google。gson。TypeAdapter;
importcom。google。gson。stream。JsonReader;
importcom。google。gson。stream。JsonWriter;
importjava。io。IOException;
importjava。util。Date;
publicclassMyDateTypeAdapterextendsTypeAdapterDate{
Override
publicvoidwrite(JsonWriterout,Datevalue)throwsIOException{
if(value){
out。Value;
}else{
out。value(value。getTime);
}
}
Override
publicDateread(JsonReaderin)throwsIOException{
if(in!){
returnnewDate(in。nextLong);
}else{
return;
}
}
}
接着,在创建Gson时,把他放入作为Date的专用处理类:GsongsonnewGsonBuilder。registerTypeAdapter(Date。class,newMyDateTypeAdapter)。create;
这样就可以让Gson将Date处理为Unix。
当然,这只是为了兼容老的缓存,如果你觉得你的仓库没有这方面的顾虑,可以忽略这个问题。
SpringBoot异常
切换到Gson后,使用SpringBoot搭建的Web项目的接口直接请求不了了。报错类似:org。springframework。http。converter。HttpMessageNotWritableException
因为SpringBoot默认的Mapper是Jackson解析,我们切换为了Gson作为返回对象后,Jackson解析不了了。
解决方案:
application。properties里面添加:PreferredJSONmappertouseforHTTPmessageconversion
spring。mvc。converters。preferredjsonmappergson
Swagger异常
这个问题和上面的SpringBoot异常类似,是因为在SpringBoot中引入了Gson,导致swagger无法解析json。
采用类似下文的解决方案(添加Gson适配器):
http:yuyublog。top20180903springbootE5BC95E585A5swagger
GsonSwaggerConfig。javaConfiguration
publicclassGsonSwaggerConfig{
设置swagger支持gson
Bean
publicIGsonHttpMessageConverterIGsonHttpMessageConverter{
returnnewIGsonHttpMessageConverter;
}
}
IGsonHttpMessageConverter。javapublicclassIGsonHttpMessageConverterextendsGsonHttpMessageConverter{
publicIGsonHttpMessageConverter{
自定义Gson适配器
super。setGson(newGsonBuilder
。registerTypeAdapter(Json。class,newSpringfoxJsonToGsonAdapter)
。serializes空值也参与序列化
。create);
}
}
SpringfoxJsonToGsonAdapter。javapublicclassSpringfoxJsonToGsonAdapterimplementsJsonSerializerJson{
Override
publicJsonElementserialize(Jsonjson,Typetype,JsonSerializationContextjsonSerializationContext){
returnnewJsonParser。parse(json。value);
}
}
MappingJsonObject作为入参异常
有时候,我们会在入参使用类似:publicResponseResultStringsubmitAudit(RequestBodyJsonObjectjsonObject){}
如果使用这种代码,其实就是使用Gson来解析json字符串。但是这种写法的风险是很高的,平常请大家尽量避免使用JsonObject直接接受参数。
在Gson中,JsonObject若是有数字字段,会统一序列化为double,也就是会把count0这种序列化成count0。0。
为何会有这种情况?简单的来说就是Gson在将json解析为Object类型时,会默认将数字类型使用double转换。
如果Json对应的是Object类型,最终会解析为MapString,Object类型;其中Object类型跟Json中具体的值有关,比如双引号的值翻译为STRING。我们可以看下数值类型(NUMBER)全部转换为了Double类型,所以就有了我们之前的问题,整型数据被翻译为了Double类型,比如30变为了30。0。
可以看下Gson的ObjectTypeAdaptor类,它继承了Gson的TypeAdaptor抽象类:
具体的源码分析和原理阐述,大家可以看这篇拓展阅读:
https:www。jianshu。compeafce9689e7d
解决方案:
第一个方案:把入参用实体类接收,不要使用JsonObject
第二个方案:与上面的解决Date类型问题类似,自己定义一个Adaptor,来接受数字,并且处理。这种想法我觉得可行但是难度较大,可能会影响到别的类型的解析,需要在设计适配器的时候格外注意。
总结
这篇文章主要是为了那些需要将项目迁移到Gson框架的同学们准备的。
一般来说,个人小项目,是不需要费这么大精力去做迁移,所以这篇文章可能目标人群比较狭窄。
但文章中也提到了不少通用问题的解决思路,比如怎么评估迁移框架的必要性。其中需要考虑到框架兼容性,两者性能差异,迁移耗费的工时等很多问题。
希望文章对你有所帮助。
参考
《如何从Fastjson迁移到Gson》
https:juejin。impost6844904089281626120
《FastJson迁移至Jackson》此文作者自己封装了工具类来完成迁移
https:mxcall。github。iopostsE5B7A5E4BD9CE7A88BE5BA8FE59198javaSEFastJsonE8BF81E7A7BBE887B3Jackson
《你真的会用Gson吗?Gson使用指南》
https:www。jianshu。compe740196225a4
json性能对比
https:github。comzysrxxjsoncomparisontreemastersrcmainjavajsoncomparison
fastjson官方文档
https:github。comalibabafastjsonwiki
易百教程
https:www。yiibai。comjackson
关注我
我是一名奋斗在一线的互联网后端开发工程师。
主要关注后端开发,数据安全,边缘计算等方向,欢迎交流。
各大平台都能找到我
微信公众号:后端技术漫谈
Github:qqxx6661
CSDN:蛮三刀把刀
知乎:后端技术漫谈
掘金:蛮三刀把刀
腾讯云社区:后端技术漫谈
博客园:后端技术漫谈
BiliBili:蛮三刀把刀
原创文章主要内容
后端开发实战(Java为主)
技术面试
算法题解数据结构设计模式
我的生活趣事
个人公众号:后端技术漫谈
难怪孩子们不爱吃菜,原来好多蔬菜里有毒?生物进化很有意思文福林妈咪你家孩子挑食不爱吃蔬菜吗?有的孩子还好,只是不喜欢吃一两种蔬菜,或者某一类蔬菜。有的孩子抗拒吃蔬菜到极致,不会让一根菜毛进到自己嘴里,让爸爸妈妈非常……
日本太太的秋季穿搭,懂得简约,更能穿出美日本太太的秋季穿搭,懂得简约,更能穿出美。岁月山河,人间有美,一身精致的穿搭,是对岁月的成全,也是对日常美丽的成全。聪明的女子,不管多少岁,都会抽出时间,花费在日常……
年夜饭不用忙,教你6道凉菜做法,10分钟上桌,比肉菜还抢手年夜饭除了大菜以外,别忘了做几道开胃的凉拌菜哦!这些小菜好吃又好做,10分钟就能做好一盘,端上桌比大鱼大肉还抢手呢!以下整理了6道凉菜的做法,分享给大家,喜欢的朋友们不妨试试吧……
多所高校最新通知,大学生2021暑假时间出炉!家长感到担忧去年由于我国受到疫情的影响,很多大学生实现了寒暑连放,享受到了史上最长的寒假,不少毕业年级的学生甚至都没能参加毕业典礼就毕业了,让我们看到了高校的另一番景象!曾几何时,高……
大学里这3个伪教师专业,毕业后不能当老师,考生需提前了解教师这个职业作为我国的传统职业,发展至今优势越来越明显,而且还是越老越吃香,所以每年在高考填报志愿的时候,经常有父母劝自己的孩子选择师范类高校!众所周知,我国是非常重视教……
掌握书写技巧,化难为易1。合理利用田字格中的辅助线在孩子认识字形和结构之前,我们应该先让孩子熟悉一下田字格。低年龄段的孩子在学习写字的过程中都会接触到田字格,它也是我们最常见的写字辅助工……
鞋子后面的小尾巴别剪掉,好百邻家政阿姨告诉你正确用途今天好百邻要给大家分享的是一个小知识,这个小知识呢,相信是很多人都不知道的,穿鞋的人都知道,鞋子后面有一个小尾巴,但是对于这个尾巴,它的作用是干嘛的,相信是多数人都不了解的。……
梦想能到的地方,脚步有一天也能到达每当看见那些中医大家写的医案,发表的文章,读起来真的让人感觉很受益。发心要把中医学好,为百姓健康献上自己的一点微薄之力。可是,面对学校的教书模式,真的让人感觉自己就是来学……
建设美丽家园助力乡村振兴近年来,青海省海西州妇联切实发挥职能作用,广泛开展美丽家园示范村建设,以村庄美、庭院美、个人美、精神美四美为创评标准,引导广大妇女主动参与农牧区人居环境整治,建设清洁、绿色、健……
首次接触一次性内裤,你会怎么选?一次性内裤是很方便的,平时很多时候都要用,所以我在家里也会准备一些一次性内裤,对于一些首次接触的人来讲,如何挑选是最值得关心的了。1。看材质普通的面料穿着不舒适,而……
霸气大姐张美玉霸气查寝火了,学生时代的噩梦近日,一则学生干部查寝摆官威的消息,引发了舆论热议。网传视频显示,六名身穿黑色正装的女子进入宿舍查寝,其中一名女子称:以后看清楚我们六个的脸,我们来了就是查寝了,看好工牌……
一个人最大的智慧,是降低对别人的期待付出真心,不一定得到真心,用心对一个人好,很可能会失落至极。在这个世上,每个人都希望得到别人的认可,可很多时候,不是你做了就一定有好结果的。感情,从来不是等价交换。……