一次Spring Bean初始化順序問題排查記錄


最近在使用Springboot的時候需要通過靜態的方法獲取到Spring容器托管的bean對象,參照一些博文里寫的,新建了個類,並實現ApplicationContextAware接口。代碼大致如下: 

@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtils.applicationContext == null) {
            SpringUtils.applicationContext = applicationContext;
        }
    }
   public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }  
}

然后另外一個bean需要依賴這個靜態獲取bean的方法,代碼大致如下:

@Component
public class TestBean{

   private Object dependencyBean = SpringUtils.getBean(OtherBean.class);  
    
}

(注: 忽略代碼邏輯是否合理~~ 這些代碼是為演示所用簡化的邏輯,肯定有同學會說:既然都是bean了為什么不注入,而是要用靜態的獲取呢?這個暫時不考慮,暫認為就必須要這樣搞)

這兩個類的層次結構和包名大致如下: 

utils

> notice

  > TestBean

> SpringUtils


就是TestBean在SpringUtils的下一級,TestBean所在包名為notice(這這個名字很重要! 直接影響到這兩個bean的加載順序,具體原理往下看

代碼就這么多,從以上代碼來靜態分析看,確實有些漏洞,因為沒有考慮到Spring bean的加載順序,可能導致的SpringUtils報空指針異常(在TestBean先於SpringUtils初始化的場景下),不管怎么樣先執行一下看下效果,效果如下: 

macOS操作系統下代碼正常

windows平台下代碼空指針異常

為什么這還跟平台有關了呢?難道Spring bean的初始化順序還跟平台有關?事實證明這個猜想是正確的。下面從Spring源代碼里來找原因。

這里需要重點關注的類是 ConfigurationClassPostProcessor,這個類是干什么的?它從哪里來?如何實現bean的加載的?

在Spring里可以指定甚至自定義多個BeanFactoryPostProcessor來實現在實例化bean之前做一些bean容器的更新操作,比如修改某些bean的定義、增加一些bean、刪除一些bean等,而ConfigurationClassPostProcessor就是Spring為了支持基於注解bean的功能而實現的BeanFactoryPostProcessor。

web環境的Springboot默認使用的應用上下文(ApplicationContext,BeanFactoryPostProcessor就是注冊到這里才會起作用的)是AnnotationConfigEmbeddedWebApplicationContext,在AnnotationConfigEmbeddedWebApplicationContext的構造方法里初始化this.reader的時候,在reader的構造方法里把ConfigurationClassPostProcessor添加到ApplicationContext里了:

 

public class AnnotationConfigEmbeddedWebApplicationContext
        extends EmbeddedWebApplicationContext {

    private final AnnotatedBeanDefinitionReader reader;

    private final ClassPathBeanDefinitionScanner scanner;

    private Class<?>[] annotatedClasses;

    private String[] basePackages;

    /**
     * Create a new {@link AnnotationConfigEmbeddedWebApplicationContext} that needs to be
     * populated through {@link #register} calls and then manually {@linkplain #refresh
     * refreshed}.
     */
    public AnnotationConfigEmbeddedWebApplicationContext() {
        this.reader = new AnnotatedBeanDefinitionReader(this);  // 重點關注這句
        this.scanner = new ClassPathBeanDefinitionScanner(this);
    }

 

public class AnnotatedBeanDefinitionReader {

    private final BeanDefinitionRegistry registry;

    private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();

    private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();

    private ConditionEvaluator conditionEvaluator;

    /**
     * Create a new {@code AnnotatedBeanDefinitionReader} for the given registry and using
     * the given {@link Environment}.
     * @param registry the {@code BeanFactory} to load bean definitions into,
     * in the form of a {@code BeanDefinitionRegistry}
     * @param environment the {@code Environment} to use when evaluating bean definition
     * profiles.
     * @since 3.1
     */
    public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        Assert.notNull(environment, "Environment must not be null");
        this.registry = registry;
        this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); // 這里把ConfigurationClassPostProcessor注冊到上下文里了
    }

上面介紹了Spring如何注冊這個ConfigurationClassPostProcessor,下面看下這個類如何實現bean定義加載的。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        // ....
        // Parse each @Configuration class
        ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment,
                this.resourceLoader, this.componentScanBeanNameGenerator, registry);

        Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
        Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
        do {
            parser.parse(candidates);
            parser.validate();

            Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
            

主要的代碼是 parser.parse(candidates); 這句,當執行到這里的時候candidates已經包含Springboot的配置類,這個parse會根據配置類里定義的basePackge遞歸掃描這個目錄下面的class文件(如果沒有定義basePackage字段則把配置類所在的包作為basePackage),debug跟蹤代碼到最底層可以看到使用的是PathMatchingResourcePatternResolver的doRetrieveMatchingFiles方法:

 
         
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("Searching directory [" + dir.getAbsolutePath() +
"] for files matching pattern [" + fullPattern + "]");
}
File[] dirContents = dir.listFiles();
if (dirContents == null) {
if (logger.isWarnEnabled()) {
logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
}
return;
}
Arrays.sort(dirContents);
for (File content : dirContents) {
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
if (!content.canRead()) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
"] because the application is not allowed to read the directory");
}
}
else {
doRetrieveMatchingFiles(fullPattern, content, result);
}
}
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
 

在這里可以看到有句 Arrays.sort(dirContents); 這個代碼,就是在遍歷一個文件夾下的資源時(包括文件夾和class文件),會先把資源排序一下,這個排序決定了bean的加載順序!

那再看下File(上面代碼中的dirContents是File列表)是如何排序的:

public class File
    implements Serializable, Comparable<File>
{

    /**
     * The FileSystem object representing the platform's local file system.
     */
    private static final FileSystem fs = DefaultFileSystem.getFileSystem();


    /* -- Basic infrastructure -- */

    /**
     * Compares two abstract pathnames lexicographically.  The ordering
     * defined by this method depends upon the underlying system.  On UNIX
     * systems, alphabetic case is significant in comparing pathnames; on Microsoft Windows
     * systems it is not.
     *
     * @param   pathname  The abstract pathname to be compared to this abstract
     *                    pathname
     *
     * @return  Zero if the argument is equal to this abstract pathname, a
     *          value less than zero if this abstract pathname is
     *          lexicographically less than the argument, or a value greater
     *          than zero if this abstract pathname is lexicographically
     *          greater than the argument
     *
     * @since   1.2
     */
    public int compareTo(File pathname) {
        return fs.compare(this, pathname);
    }

使用的是FileSystem的排序方法,再看看DefaultFileSystem.getFileSystem();拿到是是什么:

mac下:

/**
 *
 * @since 1.8
 */
class DefaultFileSystem {

    /**
     * Return the FileSystem object for Unix-based platform.
     */
    public static FileSystem getFileSystem() {
        return new UnixFileSystem();
    }
}
// UnixFileSystem

class UnixFileSystem extends FileSystem {

/* -- Basic infrastructure -- */

  public int compare(File f1, File f2) {
    return f1.getPath().compareTo(f2.getPath());
  }

}

 

windows下:

/**
 *
 * @since 1.8
 */
class DefaultFileSystem {

    /**
     * Return the FileSystem object for Windows platform.
     */
    public static FileSystem getFileSystem() {
        return new WinNTFileSystem();
    }
}
//
WinNTFileSystem

@Override
public int compare(File f1, File f2) {
  return f1.getPath().compareToIgnoreCase(f2.getPath());
}

 
        

由於windows下和mac下使用的FileSystem不同,jdk windows版FileSystem實現的compare方法在比較文件是忽略了文件名的大小寫,而mac版沒有忽略大小寫,所以導致前面提出的同樣的代碼在windows下報錯,在mac下就是正常的問題。

但是為什么會有這樣的差別呢?不是太明白為什么

那還剩最后一個問題,這個問題如何解決呢?我這有兩個方法,一是修改類名讓被依賴的類排在前面,這種方法不是太優雅,而且如果以后jdk更新了排序方法可能還會出bug,第二種是在使用類上加DependOn注解,主動說明在初始化使用類時首先加載被依賴的類,這樣就沒有問題了,但是我感覺在開發的時候盡量避免這種依賴問題,這讓容器和業務代碼參雜,以后維護是個噩夢


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM