先交代一下背景,在很久之前,我曾经封装过一个分库分表的扫表工具——Full Table Scanner,主要实现方式是通过使用TDDL Hint + 网格任务 + Mybatis Stream Query 提升性能,降低使用成本。
为了方便使用,我把他封装成了一个SpringBoot Starter,因为他提供了很好的快速扫表能力,所以被很多应用使用,并且一直都跑的好好的。
但是前两天,突然有人在钉钉上找我,说是他们应用做了改造,启动的时候报错,报错内容和我的这个工具有关。
主要报错信息如下:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.taobao.pandora.boot.loader.MainMethodRunner.run(MainMethodRunner.java:54)
at com.taobao.pandora.boot.loader.Launcher.launch(Launcher.java:87)
at com.taobao.pandora.boot.loader.Launcher.launch(Launcher.java:50)
at com.taobao.pandora.boot.loader.SarLauncher.main(SarLauncher.java:171)
Caused by: org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.alibaba.fin.xxx.test.CreditXXXStartUp]; nested exception is java.lang.IllegalStateException: Unable to read meta-data for class com.alibaba.fin.table.scanner.autoconfiguration.TDDLTableTopologyBuilderAutoConfiguration
at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:556)
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:185)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:308)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:228)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:272)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:92)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:687)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:525)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:134)
at com.alibaba.fin.xxx.start.CreditpayXXXBootStartUp.main(CreditpayMercuryApplicationBootStartUp.java:88)
... 8 more
Caused by: java.lang.IllegalStateException: Unable to read meta-data for class com.alibaba.fin.table.scanner.autoconfiguration.TDDLTableTopologyBuilderAutoConfiguration
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationMetadata(AutoConfigurationSorter.java:217)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationValue(AutoConfigurationSorter.java:198)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.readBefore(AutoConfigurationSorter.java:186)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.<init>(AutoConfigurationSorter.java:158)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClasses.<init>(AutoConfigurationSorter.java:115)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter.getInPriorityOrder(AutoConfigurationSorter.java:57)
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.sort(AutoConfigurationImportSelector.java:241)
at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.selectImports(AutoConfigurationImportSelector.java:98)
at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:547)
... 20 more
Caused by: java.io.FileNotFoundException: class path resource [ com/alibaba/fin/table/scanner/autoconfiguration/TDDLTableTopologyBuilderAutoConfiguration.class] cannot be opened because it does not exist
at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:172)
at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:50)
at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:98)
at org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory.createMetadataReader(ConcurrentReferenceCachingMetadataReaderFactory.java:89)
at org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory.getMetadataReader(ConcurrentReferenceCachingMetadataReaderFactory.java:76)
at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:93)
at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationMetadata(AutoConfigurationSorter.java:213)
... 28 more
于是我开始帮忙排查,错误信息很长,其中最关键的信息就是这句:
Caused by: java.io.FileNotFoundException: class path resource [ com/alibaba/fin/table/scanner/autoconfiguration/TDDLTableTopologyBuilderAutoConfiguration.class] cannot be opened because it does not exist
看到这个报错,最开始的反应以为是jar包冲突了。
开始进行maven的依赖分析,发现并没有什么冲突的。
为了定位问题到底和这个Starter是否有关,我们先把关于这个工具的依赖排除掉了,之后就可以正常启动了。
基本确定一定是和这个工具的引入有关。于是开始排查这个包到底做了什么会导致应用启动失败。
排查的细节就不多说了,最终经过多方排查,我们最终发现这个问题竟然和一个空格有关。
仔细看上面的报错信息,提示找不到[ com/alibaba/fin/table/scanner/autoconfiguration/TDDLTableTopologyBuilderAutoConfiguration.class]
,有没有发现,在类前面多了个空格?
后来发现,错误信息中下面这句也更为关键:
Caused by: java.lang.IllegalStateException: Unable to read meta-data for class com.alibaba.fin.table.scanner.autoconfiguration.TDDLTableTopologyBuilderAutoConfiguration
仔细观察发现,com.alibaba.fin.table.scanner.autoconfiguration.TDDLTableTopologyBuilderAutoConfiguration类前面也多了个空格?
大家都知道,应用在启动的时候,会扫描classpath下面的所有spring.factories文件,并加载其中的自动配置信息。如果在加载初始化的时候遇到什么特殊的情况,应用就会启动失败。
而上面报错的TDDLTableTopologyBuilderAutoConfiguration正是我的starter的自动配置文件。
于是打开我的spring.factories,发现在TDDLTableTopologyBuilderAutoConfiguration的配置前面,真的就多了一个空格:
为了快速定位是不是这个问题,我先把这个空格删除,然后很快帮同事打了一个SNAPSHOT的包。
同事引入了我的新包之后,重启应用,发现问题解决了。
到这个地方,基本定位到了问题,就是因为我的spring.factories文件中多了一个空格,导致应用启动失败了。
但是,为什么这个问题之前没有发现呢?这次这位同事又是改了什么东西导致问题出现了呢?
问题原因分析
根据报错内容的堆栈,我们先定位到org.springframework.core.io.ClassPathResource.getInputStream这个类在读取文件的时候失败了。
这个类是Spring中的一个文件读取的相关类。首先第一个想到是不是和Spring的版本有关。
于是我们快速到启动失败的应用的日常环境和线上环境分别看了一下打包之后的Spring的版本。
日常启动失败的机器中,打包后引用的Spring版本为:
$ls|grep spring-core
spring-core-4.3.13.RELEASE.jar
线上正常运行的机器中,打包后引用的Spring版本为:
$ls|grep spring-core
spring-core-4.3.22.RELEASE.jar
果然,这货是因为Spring的版本不同,和这位同学沟通后,得知他是改动了应用里面的一个包的依赖关系,那肯定是因为这个,导致了他的Spring版本仲裁关系发生了改变,从4.3.22.RELEASE降到了4.3.13.RELEASE。
先让这位同学去排查他的Spring仲裁的问题了,我继续分析,因为Spring版本不同,并且高版本没有这个问题,说明可能是一个Spring的bug,于是我尝试求助Google。
在这之前,同事的电脑浏览器默认是百度,我先用百度查询过,但是没查到,于是改用Google。
使用关键词:"spring.factories space trim"(这几个关键词很关键,spring.factories space是问题的根源,而且我觉得这一定是个被修复的bug,而且修复方式是用了trim)
很快就找到了一个相关内容,早在2018年有网友反馈过这个问题:
在这个ISSUE中,有官方人员给出了回复,这个问题在SPR-17413(https://jira.spring.io/browse/SPR-17413?redirect=false )中被修复:
其中提到,在4.3.21, 5.0.11, 5.1.2等版本中被修复,而我们的现象也确实是,线上正常运营的版本使用的是4.3.22。
顺藤摸瓜,查看一下Spring关于本次Bug修复的提交记录:
并且在他们的测试使用的spring-factories中也增加了空格,用于测试。
上面只是截图了一部分,其实这次bug修复,官方共提交了3次,涉及到43行代码的改动。他们在多处相关操作的地方,做了trim。
总结
本次问题的发生主要是因为在旧版本的Spring中,如果引入的starter中的spring.factories中配置内容中包含空格,会导致解析失败,进而导致应用启动失败。
这是一个已经被报告出来,并且也被解决了的bug。在SPR-17413中已经被修复,在高于4.3.21的版本中被修复。
其实,这篇文章的内容并不复杂,只是介绍了一个Bug,本来只是记录一下,并没什么可讲的。
但是,这个问题的排查过程还是值得学习的,有这样几个关键点:
- 1、从报错信息本身入手。
- 2、仔细看报错内容,不要忽略任何一个细节,哪怕是一个空格。
- 3、学会问题猜测,猜测后再验证。
- 4、请用Google!
参考资料:
https://github.com/spring-projects/spring-framework/issues/21946
https://github.com/spring-projects/spring-boot/issues/14903
https://jira.spring.io/browse/SPR-17413?redirect=false
重点 google!!!
想看H大出个封装boot-starter教程 :)