最近在使用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注解,主動說明在初始化使用類時首先加載被依賴的類,這樣就沒有問題了,但是我感覺在開發的時候盡量避免這種依賴問題,這讓容器和業務代碼參雜,以后維護是個噩夢
