背景
- 我们服务使用的是kotlin,在一次服务更新到测试环境后,前端反馈有个请求失败,报错信息是mFrom参数为空,参数结构如图1所示
图1
2. 服务序列化库使用的是jackson初步排查
- 首先在服务端抓包,再让客户端请求一次,通过抓包发现客户端请求有携带mFrom参数的
tcpdump -A -s 0 -ni any -p tcp and port 8080 -w dump.cap
- 根据客户端传参,分别起新旧服务,验证是否是新版本更新导致
curl --location 'localhost:8080/api/mobile/app/filetransfer/v1/upload' \
--header 'Content-Type: application/json' \
--data '{"senderUid":"juxpxxxxxxx","mFrom":{"fromAppCode":"swp","fromPlatform":"ios"}}'
测试后发现一样的参数在旧版本能正常请求,在新版本mFrom参数为空,看看新版本改动了啥
- 和相关方沟通后知道,此次版本除了正常的业务功能更新外,还接入了一个公共组件plugin-common,将该组件依赖从新版本代码中注释掉后,请求能正常执行,拉了该common组件代码,看看里面都做了啥,发现里面有段很可疑的json序列化配置代码
@Bean
fun objectMapper(): ObjectMapper {
return buildObjectMapper()
}
private fun buildObjectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper.registerModule(buildModule())
return objectMapper
}
将自定义的序列化配置取消后,直接返回ObjectMapper()还是不行
@Bean
fun objectMapper(): ObjectMapper {
return ObjectMapper()
}
取消自定义的jackson objectMapper bean后,请求能正常处理了,由此猜测是objectMapper在反序列化时有异常
- 手动调用objectMapper进行反序列验证,能复现问题,反序列化出来mFrom参数为null
@Autowired
private lateinit var objectMapper: ObjectMapper
fun test() {
val res = objectMapper.readValue<TransferFileInfo>("{"senderUid":"juxptjshwlpvzpwzyxxxx","mFrom":{"fromAppCode":"mp","fromPlatform":"ios"}}")
println(res)
}
疑问:
- 为什么手动将objectMapper注入到spring容器中会导致mFrom为空?
- 为什么只有mFrom这个参数有问题,其他参数反序列化没问题?
debug代码
先把代码切换到有问题的版本,手动注入bean进行排查,看mFrom参数为什么反序列化后为空
@Autowired
private lateinit var objectMapper: ObjectMapper
fun test() {
val res = objectMapper.readValue<TransferFileInfo>("{"senderUid":"juxptjshwlpvzpwzyxxxx","mFrom":{"fromAppCode":"mp","fromPlatform":"ios"}}")
println(res)
}
debug到com.fasterxml.jackson.databind.deser.BeanDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
如图2所示,com.fasterxml.jackson.databind.deser.BeanDeserializer#vanillaDeserialize这个方法,其他参数都能找到prop, mFrom参数却找不到,看看_beanProperties的find逻辑
图2
如图3所示,com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap#find(java.lang.String),发现match居然是mfrom,和我们定义的参数名mFrom不一样
图3
如图4所示,_hashArea只有两个变量,并没有我们定义的mFrom参数,debug看下_hashArea是如何初始化的
图4
往上debug链路,如图5所示,发现该prop是通过BeanDescription解析出来的,看名字大概率就是解析类出来了问题,继续看BeanDescription的_properties是如何生成的
com.fasterxml.jackson.databind.deser.DeserializerCache#_createDeserializer
图5
如图7所示,_properties是通过collector返回的,看下collector都做了啥
com.fasterxml.jackson.databind.introspect.BasicBeanDescription#_properties
图6
以下是props解析的代码,具体的debug逻辑如图7-9所示,可以看到一开始是能正常解析到mFrom参数的,可是在经过_addMethods方法后多了一个mfrom参数
com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#collectAll
protected void collectAll()
{
LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();
_addFields(props); // addFields方法执行后能找到声明的两个变量,senderUid和mFrom
_addMethods(props); // 执行完该方法props size变成了3,多了个mfrom参数
if (!_classDef.isNonStaticInnerClass()) {
_addCreators(props);
}
_removeUnwantedProperties(props); // 执行完该方法后,props里面的mFrom参数没了,因为jackson找不到该参数对应的set、get、构造函数参数位置
_removeUnwantedAccessor(props);
_renameProperties(props);
}
图7
图8
图9
排查到这里,猜测是解析多了一个mfrom参数,所以导致mFrom被移除了,但是切到旧版本后debug发现,该猜测不对,旧版本也是会多排出一个mfrom参数,和新版的区别在与_removeUnwantedPropertie方法执行完,旧版本不会将原来props的mFrom参数移除
排查到这里,疑问更多了
- 为什么手动将objectMapper注入到spring容器中会导致mFrom为空?
- 为什么只有mFrom这个参数有问题,其他参数反序列化没问题?
- 为什么执行完addMethods多了一个mfrom参数?
- 为什么找不到mFrom任何set、get、构造函数位置参数?
- 为什么新旧版本_removeUnwantedPropertie的处理逻辑不同,新版会将mFrom参数移除、旧版不会?
先来看最关键的_removeUnwantedPropertie方法,看看两个版本的处理区别
旧版本_anyVisible(_ctorParameters)为true,新版本为false
com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder#anyVisible
protected Linked<AnnotatedParameter> _ctorParameters;
public boolean anyVisible() {
return _anyVisible(_fields)
|| _anyVisible(_getters)
|| _anyVisible(_setters)
|| _anyVisible(_ctorParameters)
;
}
通过debug, 发现新旧版本主要差别是在findImplicitPropertyName方法,关键代码如下,旧版本impl返回mFrom参数(图10),新版没有(图11),
com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#_addCreators
com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#_addCreatorParam
protected void _addCreatorParam(Map<String, POJOPropertyBuilder> props,
AnnotatedParameter param)
{
String impl = _annotationIntrospector.findImplicitPropertyName(param);
if (impl == null) {
impl = "";
}
_creatorProperties.add(prop);
}
图10
图11
旧版本是在com.fasterxml.jackson.module.paramnames.ParameterNamesAnnotationIntrospector这个类找到mFrom参数的,看看这个类的来源
com.fasterxml.jackson.module.paramnames.ParameterNamesModule#setupModule
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.insertAnnotationIntrospector(new ParameterNamesAnnotationIntrospector(creatorBinding, new ParameterExtractor()));
}
发现该方法的调用入口是在springboot的自动配置类
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
这也就解释了为什么common组件手动生成bean后,_annotationIntrospector.findImplicitPropertyName会找不到mFrom参数,@ConditionalOnMissingBean发现已经有ObjectMapper就不会走默认的配置逻辑
由于我们手动声明objectMapper是因为需要自定义一些序列化规则,为了不动到springboot默认的objectMapper,那我们是否能把自定义的规则手动往springboot默认的objectMapper添加?
如以下代码所示,springboot其实也有提供一些扩展点,Jackson2ObjectMapperBuilderCustomizer该接口提供了拓展点
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.JacksonObjectMapperBuilderConfiguration#jacksonObjectMapperBuilder
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
@Configuration
class CustomConfig: Jackson2ObjectMapperBuilderCustomizer {
override fun customize(builder: Jackson2ObjectMapperBuilder) {
builder.serializationInclusion(JsonInclude.Include.NON_NULL)
builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
builder.serializerByType(LocalDateTime::class.java, LocalDateTimeToLongSerializer())
builder.deserializerByType(LocalDateTime::class.java, LongToLocalDateTimeDeserializer())
}
}
该配置只在springboot的时候才生效,但是有些依赖common组件的应用不一定需要使用springboot,因此为了满足这种场景,我们需要另外再实现一套jackson序列化注册规则
val objectMapper: ObjectMapper by lazy { buildObjectMapper() }
private fun buildObjectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper.registerModule(buildModule())
return objectMapper
}
fun buildModule(): Module {
val module = SimpleModule()
// 修改不带无参构造的类仍能被实例化
module.setDeserializerModifier(buildBeanDeserializerModifier())
module.addSerializer(LocalDateTime::class.java, LocalDateTimeToLongSerializer())
module.addDeserializer(LocalDateTime::class.java, LongToLocalDateTimeDeserializer())
return module
}
module有大量重复的功能因为实现不同不能重复使用,因此看springboot注入objectMapper时,依赖的jackson module是如何注册的
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer#configureModules
private void configureModules(Jackson2ObjectMapperBuilder builder) {
Collection<Module> moduleBeans = getBeans(this.applicationContext, Module.class);
builder.modulesToInstall(moduleBeans.toArray(new Module[0]));
}
将自定义的module注入到spring中即可减少Jackson2ObjectMapperBuilderCustomizer重复代码
@ConditionalOnClass(JacksonAutoConfiguration::class)
@Bean
fun registerCustomModule(): Module {
return buildModule()
}
@Configuration
class CustomConfig: Jackson2ObjectMapperBuilderCustomizer {
override fun customize(builder: Jackson2ObjectMapperBuilder) {
builder.serializationInclusion(JsonInclude.Include.NON_NULL)
builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}
}
到此自定义的objectMapper配置都已注册到springboot默认的objectMapper,运行下试下
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No fallback setter/field defined for creator property 'mFrom' (through reference chain: com.example.plugindemo.TransferFileInfo["mFrom"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.deser.CreatorProperty._reportMissingSetter(CreatorProperty.java:354)
at com.fasterxml.jackson.databind.deser.CreatorProperty._verifySetter(CreatorProperty.java:341)
at com.fasterxml.jackson.databind.deser.CreatorProperty.deserializeAndSet(CreatorProperty.java:270)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:314)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:177)
at com.**.ifpd.plugin.common.json.JsonConfiguration$ObjenesisJsonDeserializer.deserialize(JsonConfiguration.kt:130)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4674)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3629)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3612)
反序列化时mFrom参数是找到了,结果却报了mFrom没有set方法,这个问题在之前的旧版本是没有的,通过debug发现新旧版本在反序列化时的差异,如图12所示 com.fasterxml.jackson.databind.deser.BeanDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
图12
而导致该区别则是common自定义了反序列化规则,用于解决类没有无参构造时的反序列化,以下是common组件的代码
module.setDeserializerModifier(buildBeanDeserializerModifier())
private fun buildBeanDeserializerModifier() = object : BeanDeserializerModifier() {
override fun updateBuilder(
config: DeserializationConfig,
beanDesc: BeanDescription,
builder: BeanDeserializerBuilder
): BeanDeserializerBuilder {
builder.valueInstantiator = object : StdValueInstantiator(config, beanDesc.type) {
override fun createUsingDefault(ctxt: DeserializationContext?): Any {
return NewInstanceUtils.instantiate(beanDesc.beanClass)
}
}
return builder
}
}
如图13所示,加上以上配置后jackson会先通过反射生成实例,再通过set方法将值设置到实例
图13
之前我们留有个疑问:为什么找不到mFrom如何set、get、构造函数位置参数? 构造函数位置我们知道了,因为自定义的序列化配置覆盖了springboot默认的,但是set、get方法呢,不管是新旧版本都找不到,因为之前的旧版本没有用到set方法(因为没有走无参构造反序列化逻辑),所以没有发现这个问题,通过debug后发现问题出在 com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#_addMethods,如图14所示,可以看到一开始是能找到mFrom的set、get方法的
图14
如图15所示,jackson会通过以上解析到的set、get生成对应的参数名去绑定值,com.fasterxml.jackson.databind.introspect.DefaultAccessorNamingStrategy#findNameForRegularGetter
图15
debug到 com.fasterxml.jackson.databind.introspect.DefaultAccessorNamingStrategy#legacyManglePropertyName,当basename为getMFrom,该方法最后生成的参数名是mfrom,自然set、get也就没办法和我们原先正确的mFrom参数绑定起来
// baseName为getMFrom, offset为3
protected String legacyManglePropertyName(final String basename, final int offset)
{
final int end = basename.length(); // end为8
if (end == offset) {
return null;
}
char c = basename.charAt(offset); // M
if (_baseNameValidator != null) { // false
if (!_baseNameValidator.accept(c, basename, offset)) {
return null;
}
}
char d = Character.toLowerCase(c); // m
if (c == d) { // false
return basename.substring(offset);
}
StringBuilder sb = new StringBuilder(end - offset);
sb.append(d); // "m"
int i = offset+1; // 4
for (; i < end; ++i) {
c = basename.charAt(i);
d = Character.toLowerCase(c);
if (c == d) {
sb.append(basename, i, end);
break;
}
sb.append(d);
}
return sb.toString();
}
为何jackson要这样处理? 查阅相关资料后发现,在java bea规范中,如果是单字母开头的驼峰命名变量,get和set方法应该设置成如setmFrom和getmFrom这样,但是kotlin data class生成的命名却是getMFrom和setMFrom,这也就导致了和jackson解析规则冲突,为了解决这个方法,那就要自定义根据方法名查找变量的逻辑,不要让jackson自己根据方法名生成,而jackson刚好就提供了这个扩展点给我们,如图16所示
图16
以下为common组件自定义的方法名解析逻辑,通过该方法则根据getMFrom则可正确返回mFrom参数,运行后也正常。
@Configuration
@ConditionalOnClass(JacksonAutoConfiguration::class)
class JacksonMethodNameCustomAnalyzer {
@Autowired
fun addMethodNameAnnotationIntrospector(objectMapper: ObjectMapper) {
val methodNameAnnotationIntrospector = MethodNameAnnotationIntrospector()
objectMapper.setAnnotationIntrospectors(
createPair(objectMapper.serializationConfig, methodNameAnnotationIntrospector),
createPair(objectMapper.deserializationConfig, methodNameAnnotationIntrospector)
)
}
private fun createPair(
config: MapperConfig<*>,
methodNameAnnotationIntrospector: AnnotationIntrospector
): AnnotationIntrospector {
return AnnotationIntrospector.pair(config.annotationIntrospector, methodNameAnnotationIntrospector)
}
}
class MethodNameAnnotationIntrospector: AnnotationIntrospector() {
override fun version(): Version {
return PackageVersion.VERSION
}
override fun findImplicitPropertyName(m: AnnotatedMember): String? {
if (m is AnnotatedMethod) {
val methodName = m.name
if (methodName.length >= 6) {
// 取第4、5位
val prefix = methodName.substring(3, 5)
if (prefix.all { it.isUpperCase() } && methodName[5].isLowerCase()) {
return methodName[3].lowercase() + methodName.substring(4)
}
}
}
return null
}
}
回顾我们之前提到的几个疑问
- 为什么手动将objectMapper注入到spring容器中会导致mFrom为空?
- 为什么只有mFrom这个参数有问题,其他参数反序列化没问题?
- 为什么执行完addMethods多了一个mfrom参数?
- 为什么找不到mFrom任何set、get、构造函数位置参数
- 为什么新旧版本_removeUnwantedPropertie的处理不同参数
答案
- 手动声明的objectMapper导致springboot默认的objectMapper配置没有生效,走了和之前旧不一样的逻辑
- mFrom参数生成的get、set方法名不符合java bean规范,jackson解释不了
- jackson根据方法名getMFrom翻译出来的参数名是mfrom,所以加到了参数列表里面
- 命名规范、springboot默认配置覆盖问题
- springboot默认配置覆盖问题
ps: 以上mFrom找不到set、get、构造函数位置的问题,直接使用JsonProperty其实也可以解决问题,但是考虑到项目中已经有多个地方使用单字母开头的驼峰命名,为了避免遗漏,以及手动在代码里面硬编码,所以才考虑在组件内完成这些事
data class TransferFileInfo(
var senderUid: String?,
/**
* 来源
*/
@get:JsonProperty(value = "mFrom")
var mFrom: MsgFromInfo
) : Serializable {