• 一次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注解,主动说明在初始化使用类时首先加载被依赖的类,这样就没有问题了,但是我感觉在开发的时候尽量避免这种依赖问题,这让容器和业务代码参杂,以后维护是个噩梦

  • 相关阅读:
    Java后端WebSocket的Tomcat实现
    Swift学习资源
    jwplayer 隐藏属性方法记载
    简单的java socket 示例
    SQL SERVER 2008/2012/2012R2/2014 设置开启远程连接(sa配置)
    JS原型继承和类式继承
    理解js中的原型链,prototype与__proto__的关系
    C# 事件(Event)
    Java 与 JavaScript 对websocket的使用
    C# 版dll 程序集合并工具
  • 原文地址:https://www.cnblogs.com/caiyao/p/10096263.html
Copyright © 2020-2023  润新知