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