一、背景简介

项目代码通过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 报错代码排查

根据上述报错堆栈信息,找到项目报错的代码,如下:
报错源码1
报错源码2

二、问题排查

由于该错误在不同环境下出现,且为开发环境,所以开启远程Debug定位问题原因。

2.1 开启远程Debug

在本地IDEA中开启远程Debug进行问题排查。

2.1.1 服务端配置

服务端的启动脚本添加如下配启动参数:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

远程Debug参数

2.1.2 本地配置

IDEA中创建一个Remote JVM Debug:
远程Debug本地配置

2.2 远程Debug排查问题

通过Debug查看类的初始化顺序,部分截图如下:
远程Debug代码1
远程Debug代码2
远程Debug代码3

2.3 排查结论

测试环境NMetaWebServiceImpl扫描顺序在ApplicationContextKeeper前面,Spring根据Bean的扫描顺序进行类的初始化,由于NMetaWebServiceImpl扫描顺序在ApplicationContextKeeper前面,所以在初始化NMetaWebServiceImpl的过程中由于ApplicationContextKeeper还未初始化,导致空指针问题。

三、本地问题复现

上述问题仅在通过Jenkins构建的包启动出现问题,本地手动构建的包并无此问题,通过Debug排查本地程序类的初始化情况。
本地Debug代码1
本地Debug代码2

3.1 排查结论

在本地IDEA中ApplicationContextKeeper扫描顺序在NMetaWebServiceImpl前面,所以程序启动是正常的。

四、Spring类加载顺序分析

参考文章:springboot bean加载顺序问题

4.1 Debug追踪Spring类加载顺序

通过Debug继续追踪类从jar包的加载顺序
Spring加载类源码1
Spring加载类源码1
Spring加载类源码1
Spring加载类源码1
Spring加载类源码1
Jar包中类的加载是一个Native方法,需要看看Native方法的class读取顺序。

4.2 Java Native方法源码分析

这里主要涉及两个C文件:ZipFile.czip_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打包

经过验证类的添加顺序是正常的,即在相同的服务器上手动执行Maven打包命令和Jenkins执行Maven打包命令所构建的Jar包中Class的顺序不一致。

在Jenkins工作路径下手动打包,问题复现。
测试环境Maven打包

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。