排坑:kotlin对象序列化过程参数丢失
问题现象与背景:
这个bug出现在引入kotlin代码的过程中。我们的项目采用的是spring boot框架,为前端提供业务接口。通过周密调研,我们计划在一个子模块中应用kotlin来进行业务开发。springboot与kotlin的结合方式不再本文累述。
开发过程中发现了一个bug:系统提供的接口有部分参数丢失。如图所示,输出的接口数据中没有is_try_read、is_online、is_free这3个数据。
进一步进行debug,发现内存中该对象确实包含对应属性。
方法已经运行到了return这一步,那么为什么在输出环节丢失了数据呢?
排查与定位:
首先分析该输出对象的类的设计是否有异常。该对象的类采用kotlin的数据类实现。
Kotlin 可以创建一个只包含数据的类,关键字为data。编译器会自动的从主构造函数中根据所有声明的属性生产equals()、hashCode()、toString() 、componentN()、和copy() 函数。kotlin的数据类简化了这种用于表示特定实体的类,不用再麻烦地设置setter、getter方法。根据kotlin代码设计原则,属性默认为public级别可见性,编译器会为其生成setter、getter方法。如图所示的数据类设计应该没有问题。
接着我反编译数据类class文件,发现了一些端倪。如图所示,"is"开头的变量,编译器自动加上set、get方法时,命名规则和其他在变量首部添加"set"和"get"字母的驼峰方式有所不同,例如is_online变量自动生成的方法为set_online()和is_online(),isOnline变量自动生成方法为setOnline()和isOnline()。这本身无可厚非,但是和某些java框架、工具结合起来就显现出了问题。
综上,我觉得可能spring boot默认的序列化工具在把对象转换成json字符串时可能找不到上述"is"开头的变量的get方法才导致该变量丢失。于是我写了一个简单的测试方法来测试fastJson、Gson、Jackson这3个序列化工具。如图所示。
其中Book是一个kotlin数据类,从输出结果发现:只有Gson保留了完整数据。
解决方案:
至此,定位到了问题原因是kotlin对"is"开头变量编译生成的set、get方法命名方式有所不同导致某些java序列化工具不能识别。因为项目用的序列化工具是jackson,所以我升级了jackson的版本,发现变量仍然丢失。
解决方案一:
数据类不用"is"开头来命名变量。但是这个始终留下了隐患,开发人员稍不注意可能就掉坑里了,治标不治本。
解决方案二:
用Gson替换框架的序列化工具。在典型的Spring场景中,一旦请求退出后,那么@Controller注解就会去渲染一个视图。我们可以通过@RequestBody或者@RestController这种注解来请求Spring,并且可以直接将Model中的java对象注入到Response中,而这个过程中应用了序列化。我采用HttpMessageConverters来自定义gson为定首选序列化工具。配置如下:
@Configuration
publicclass CustomConfiguration {
@Bean
public HttpMessageConverterscustomConverters() {
Collection<HttpMessageConverter<?>> messageConverters = newArrayList<>();
GsonHttpMessageConvertergsonHttpMessageConverter = new GsonHttpMessageConverter();
messageConverters.add(gsonHttpMessageConverter);
return new HttpMessageConverters(true,messageConverters);
}
}
通过这个配置,springframework框架默认使用的HttpMessageConverter中追加了GsonHttpMessageConverter来进行序列化工作。
最终我采用了方案二的方式来处理这个问题。
其他:
定位问题过程中其实还有一些曲折,例如is_try_read、is_online、is_free这3个变量都是kotlin的Int类型,而kotlin的Int与java的int和Integer相对应,怀疑可能是这个类型出现的序列化问题。但是在控制变量多次尝试后,发现与类型无关,String类型的is_online变量依然会丢失。
Gson提供了流式序列化的方法,核心是各种adapter的write方法。
通过getAdapter(TypeToken<T> type)方法来绑定对象的变量到其Adapter的boundFields来进行后续转换。
参考资料:
《Configure gson in spring using GsonHttpMessageConverter》