Spring类加载顺序异常Bug排查
一、背景简介
项目代码通过Jenkins自动打包构建,自动在测试环境部署时出现报错,但是该项目在本地IDEA中能够正常启动。报错信息如下:
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'nMetaWebServiceImpl': Invocation of init method failed; nested exception is java.lang.NullPointerException
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:136)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:408)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
at ***.RestServer.main(RestServer.java:25)
Caused by: java.lang.NullPointerException
at ***.webservice.service.impl.NMetaWebServiceImpl.init(NMetaWebServiceImpl.java:31)
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 org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:354)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:305)
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:133)
... 14 more
1.1 报错代码排查
根据上述报错堆栈信息,找到项目报错的代码,如下:
二、问题排查
由于该错误在不同环境下出现,且为开发环境,所以开启远程Debug定位问题原因。
2.1 开启远程Debug
在本地IDEA中开启远程Debug进行问题排查。
2.1.1 服务端配置
服务端的启动脚本添加如下配启动参数:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
2.1.2 本地配置
IDEA中创建一个Remote JVM Debug:
2.2 远程Debug排查问题
通过Debug查看类的初始化顺序,部分截图如下:
2.3 排查结论
测试环境NMetaWebServiceImpl扫描顺序在ApplicationContextKeeper前面,Spring根据Bean的扫描顺序进行类的初始化,由于NMetaWebServiceImpl扫描顺序在ApplicationContextKeeper前面,所以在初始化NMetaWebServiceImpl的过程中由于ApplicationContextKeeper还未初始化,导致空指针问题。
三、本地问题复现
上述问题仅在通过Jenkins构建的包启动出现问题,本地手动构建的包并无此问题,通过Debug排查本地程序类的初始化情况。
3.1 排查结论
在本地IDEA中ApplicationContextKeeper扫描顺序在NMetaWebServiceImpl前面,所以程序启动是正常的。
四、Spring类加载顺序分析
4.1 Debug追踪Spring类加载顺序
通过Debug继续追踪类从jar包的加载顺序
Jar包中类的加载是一个Native方法,需要看看Native方法的class读取顺序。
4.2 Java Native方法源码分析
这里主要涉及两个C文件:ZipFile.c
和 zip_util.c
。
ZipFile.c
JNIEXPORT jlong JNICALL
Java_java_util_zip_ZipFile_getNextEntry(JNIEnv *env, jclass cls, jlong zfile,
jint n)
{
jzentry *ze = ZIP_GetNextEntry(jlong_to_ptr(zfile), n);
return ptr_to_jlong(ze);
}
zip_util.c
/*
* Returns the n'th (starting at zero) zip file entry, or NULL if the
* specified index was out of range.
*/
jzentry * JNICALL
ZIP_GetNextEntry(jzfile *zip, jint n)
{
jzentry *result;
if (n < 0 || n >= zip->total) {
return 0;
}
ZIP_Lock(zip);
result = newEntry(zip, &zip->entries[n], ACCESS_SEQUENTIAL);
ZIP_Unlock(zip);
return result;
}
/*
* Descriptor for a ZIP file.
*/
typedef struct jzfile { /* Zip file */
char *name; /* zip file name */
jint refs; /* number of active references */
jlong len; /* length (in bytes) of zip file */
#ifdef USE_MMAP
unsigned char *maddr; /* beginning address of the CEN & ENDHDR */
jlong mlen; /* length (in bytes) mmaped */
jlong offset; /* offset of the mmapped region from the
start of the file. */
jboolean usemmap; /* if mmap is used. */
#endif
jboolean locsig; /* if zip file starts with LOCSIG */
cencache cencache; /* CEN header cache */
ZFILE zfd; /* open file descriptor */
void *lock; /* read lock */
char *comment; /* zip file comment */
jint clen; /* length of the zip file comment */
char *msg; /* zip error message */
jzcell *entries; /* array of hash cells */
jint total; /* total number of entries */
jint *table; /* Hash chain heads: indexes into entries */
jint tablelen; /* number of hash heads */
struct jzfile *next; /* next zip file in search list */
jzentry *cache; /* we cache the most recently freed jzentry */
/* Information on metadata names in META-INF directory */
char **metanames; /* array of meta names (may have null names) */
jint metacurrent; /* the next empty slot in metanames array */
jint metacount; /* number of slots in metanames array */
jint manifestNum; /* number of META-INF/MANIFEST.MF, case insensitive */
jlong lastModified; /* last modified time */
jlong locpos; /* position of first LOC header (usually 0) */
} jzfile;
4.3 分析结果
通过上述Native方法的源码分析,可以看到class文件的顺序依赖于jzcell *entries这个数据结构,猜想到这个数据结构为Jar包构建时类文件的添加顺序。
五、Jar包Class文件顺序分析
查看测试环境与本地环境的Jar包中Class的顺序。
5.1 测试环境
5.2 本地环境
5.3 结果分析
不同环境中的Jar包其中类的添加顺序不一致,而Spring根据Jar包中的类的顺序进行类的初始化,导致测试环境启动报错。
六、Jar包打包Class顺序验证
根据上述排查结果,我们知道是由于在不同环境下项目中打包的Jar包中Class顺序不一致引起的问题,接下来分析一下为什么不同环境下打包,Class的顺序会不同。
6.1 手动打包验证
我们知道Jar包的本质是一个压缩包,所以首先验证手动构建压缩包时文件的顺序。
tar -zcvf mtr-rest.jar ./
zip -r mtr.rest.jar ./
使用tar 命令或zip命令手动制作压缩包,文件按照首字母顺序排序。所以Maven构建Jar包的方式与上述两个命令不同。
6.2 测试环境验证
在测试服务器上新建一个目录,将项目克隆下来进行打包。
经过验证类的添加顺序是正常的,即在相同的服务器上手动执行Maven打包命令和Jenkins执行Maven打包命令所构建的Jar包中Class的顺序不一致。
在Jenkins工作路径下手动打包,问题复现。
6.3 问题分析
经过上述验证,发现在服务器的不同路径下进行打包,Jar包中的Class的添加顺序不一致。
七、排查结论
1.Maven默认使用不稳定的文件系统遍历来收集并构建项目的类。这可能导致在不同操作系统、不同环境或不同构建命令下,类的顺序出现变化。
2.从上述的验证结果来看,项目的父路径将会影响Maven打包类的添加顺序。
3.此外类的依赖顺序应该通过应用层面去保证,而不应该依赖于jar中类的物理路径。
八、解决方案
上述问题主要是由于两个类之间没有构建依赖关系,正常当按照文件排序的方式实例化两个类时,程序正常运行;但是当打包时,两个类添加到Jar包的物理顺序出现变化,则会出现异常。
8.1 显示添加依赖关系
在NMetaWebServiceImpl类上使用@DependsOn注解,让ApplicationContextKeeper先于NMetaWebServiceImpl初始化。
8.2 隐式构建依赖关系
将ApplicationContextKeeper实例注入到NMetaWebServiceImpl,隐式构建类的依赖关系。
8.3 优先初始化
由于NMetaWebServiceImpl是一个webservice应用,实例对象没有在spring中,而是通过类的静态成员变量进行传递数据,无法直接构建依赖管理,所以在spring中优先实例化ApplicationContextKeeper。