从Bean的加载顺序教你如何看Spring源码

·  阅读 1353

为什么要写这篇文章

因为 @你说吧好吗 同学指出了我之前的一篇文章《教你手写全局异常拦截器》一个关于Bean加载顺序的一个小BUG,通过分析这个案例,顺路教一下大家如何看Spring源码。

BUG复现

代码我就不贴了,有兴趣的可以去GitHub下载源码,也可以看我之前那篇博文《教你手写全局异常拦截器》

项目目录如下

我们启动项目发现没什么问题,也没有报错,但是为什么说这里隐藏得有一个bug呢?

我们把ExceptionConfig这个类名改成ExceptionSconfig然后再启动看一下

报错了,ExceptionMethodPool找不到,看到这儿你可能就觉得很奇怪了,换了个类名会导致代码出问题,这是什么原因?

问题分析

报错信息比较明显,bean找不到,于是我们去看代码,找到注入ExceptionMethodPool注入的地方

@Component
public class ExceptionBeanPostProcessor implements BeanPostProcessor {
    private ExceptionMethodPool exceptionMethodPool;
    @Autowired
    private ConfigurableApplicationContext context;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> clazz = bean.getClass();
        ExceptionAdvice advice = clazz.getAnnotation(ExceptionAdvice.class);
        if (advice == null) return bean;
        if (exceptionMethodPool != null) throw new RuntimeException("不允许有两个异常定义类");
        exceptionMethodPool = new ExceptionMethodPool(bean);

        //保持处理异常方法顺序
        Arrays.stream(clazz.getDeclaredMethods())
                .filter(method -> method.getAnnotation(ExceptionHandler.class) != null)
                .forEach(method -> {
                    ExceptionHandler exceptionHandler = method.getAnnotation(ExceptionHandler.class);
                    Arrays.stream(exceptionHandler.value()).forEach(c -> exceptionMethodPool.add(c,method));
                });
        //注册
        context.getBeanFactory().registerSingleton("exceptionMethodPool",exceptionMethodPool);
        return bean;
    }
}
复制代码

这个方法的作用是找到异常处理器,并把异常处理器丢到ExceptionMethodPool,最后把ExceptionMethodPool丢到IOC容器。

看到这里,我们很容易想到是spring bean加载顺序导致了这个问题,那么Spring Bean的加载顺序到底是怎样的呢?

ok,我带着大家看看如何从源码中找到答案。

如何看源码

我总结了我自己看源码的方式,大家可以参考下

  1. 从问题入手,只看和问题相关的部分
  2. 遇到看不懂的地方不要纠结
  3. 先猜想,再从源码验证猜想

我们先来分析一下,我们现在知道或者能够推测出来的一些结论

  1. 首先, Bean加载应该是有一定的加载顺序
  2. 由于Bean的属性可以注入别的Bean,但是Bean的加载是有一定顺序的,那么,遇到没有加载的Bean,Spring一定是有一套处理方法的
  3. 基于上述的例子,我们用BeanPostProcessor的钩子方法注入bean,是会影响到Bean的加载的。

ok,我们从问题入手,去分析Spring是按照怎样的顺序去加载Bean的

首先,我们先在注入exceptionMethodPool,打个断点,看看我们能够得到什么信息

乍一看,看不出啥,这时候我们要关注左下角的信息,这是代码的调用链路,我们可以通过点击左下角的链路来看看这个代码是如何运行到这里来的。

我们来看一下applyBeanPostProcessorsBeforeInitialization这个方法,可以看到,对于exceptionConfig这个Bean,Spring是循环调用了所有的BeanPostProccessor去对其进行处理,实际上BeanPostProccessor也是一个Bean,我们不难推测出BeanPostProccessor应该是优先加载的,我们记住这个结论,后续在验证。

接下来继续看调用链

从这里我们可以看到在初始化Bean的时候是先调用BeanPostProcessor的前置处理器、然后是初始化方法,接下来是后置处理器,不管后面用不用得到,先记下这个结论。

接着看

注意了,这个时候我们看到了一个非常重要的一个信息

populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
复制代码

我们可以看到,spring先进行了依赖注入,然后再执行前置处理、初始化方法、后置处理,这是一个非常重要的结论。

之前提到过,Bean在进行加载的时候,如果依赖的bean还没有加载,Spring一定会有一个处理方式,我们来看看Spring如何去处理

点进populateBean,我们可以看到一段非常显眼的代码

if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
   MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
   // Add property values based on autowire by name if applicable.
   if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_NAME) {
      autowireByName(beanName, mbd, bw, newPvs);
   }
   // Add property values based on autowire by type if applicable.
   if (mbd.getResolvedAutowireMode() == AUTOWIRE_BY_TYPE) {
      autowireByType(beanName, mbd, bw, newPvs);
   }
   pvs = newPvs;
}
复制代码

按照名称或者是按照类型注入

接着点进autowiteByName()

public boolean containsBean(String name) {
		String beanName = transformedBeanName(name);
		if (containsSingleton(beanName) || containsBeanDefinition(beanName)) {
			return (!BeanFactoryUtils.isFactoryDereference(name) || isFactoryBean(name));
		}
		// Not found -> check parent.
		BeanFactory parentBeanFactory = getParentBeanFactory();
		return (parentBeanFactory != null && parentBeanFactory.containsBean(originalBeanName(name)));
}
复制代码

如果bean容器和beanDefinition容器都没有说明不存在,如果有的话就调用getBean()获取bean进行注入。

getBean()的作用是获取bean,如果bean存在就直接返回存在的,不存在就加载(针对单例),由于这篇文章并不是分析bean的加载过程,这里就不做过度展开了。

接着跟getBean() ->doGetBean()方法,我们能够看到还有一段代码其实也是影响bean的加载顺序的

String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
  for (String dep : dependsOn) {
    if (isDependent(beanName, dep)) {
      throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                      "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
    }
    registerDependentBean(dep, beanName);
    try {
      getBean(dep);
    }
    catch (NoSuchBeanDefinitionException ex) {
      throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                                      "'" + beanName + "' depends on missing bean '" + dep + "'", ex);
    }
  }
}
复制代码

这里从BeanDefinition里面拿出这个bean依赖的bean,然后挨个的去加载

这个dependsOn实际上就是下图这个注解里面的值

到这里,我们可以得出一个结论,spring bean加载的时候,遇到没有加载的bean会去触发bean的加载,换句话说,被依赖的bean是优先加载的。

看到这里,我们实际上还是有疑虑的

  1. 为什么改了文件名,程序就出错了?
  2. 为什么BeanProcessor也是bean,为啥他优先加载?

带着问题,我们接着看

我们跟到这里,发现我们的bean是通过finishBeanFactoryInitialization去进行加载的,但是前面还有非常多的处理

// Register bean processors that intercept bean creation.
// 注册beanProcessor
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
// 初始化MessageSoruce
initMessageSource();

// Initialize event multicaster for this context.
//初始化事件分发器
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
// 初始化一些特殊的bean
onRefresh();

// Check for listener beans and register them.
//初始化listener
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
// 初始化剩余非懒加载的bean 实际上我们的业务bean都是属于这一类
finishBeanFactoryInitialization(beanFactory);
复制代码

我们可以看到,其实bean是先按照不同的类型进行加载的。

那么还有一个问题没有解决,为毛我换了一下文件名,我的程序就报错了?

那就接着看

我们可以看到,bean是根据beanNames的顺序迭代去加载的,正常启动的时候,exceptionConfig顺序是9,exceptionProcessor的顺序是11,所以可以正常启动

我们看到异常启动的时候,exceptionProcessor是在exceptionSonfig之前启动的,这个时候ExceptionMethodPool还没有注入IOC容器,所以报了错。

ok,看到这儿我们已经知道了bean的默认加载顺序是和 this.beanDefinitionNames 这个东西有关,找到这个东西加载的逻辑就解决了这个问题。

我们通过工具看一下,这个集合里面的数据是在哪儿塞进去的

通过分析代码,发现上两个箭头是可能对该集合进行新增的地方,下两个是对集合进行删除的地方,因此我们重点看前面的,发现这两个集合都是在一个方法里面,因此我们在这个方法上面断点

我们可以看到最先加载的并不是我们自己定义的bean,考虑到spring会先加载一些内置的东西,我们先把这部分按f9跳过,等开始加载我们自己定义的资源的时候再看

我们可以看到开始加载启动类了,但是没有发现什么太多有价值的信息,按f9接着看

当加载到testController的时候我们可以看到一个非常重要的信息,doScan()看起来像是在扫描我们的项目,ok,点进去看下

可以看到,是遍历这个candidates去进行注册的,我们注意一下这个candidate里面元素的顺序和之前异常启动时候的beanDefinitionNames 里面元素的顺序是一致的。

我们注意到,这个candidates是一个Set,Set遍历的顺序是与Hashcode有关的,我们改变了类名,导致HashCode发生的变化,于是遍历Set的顺序发生了变化,最终就导致了程序报错。

总结一下这个bug出现的原因

当类名为ExceptionConfig时,ExceptionConfig是优先于ExceptionProcessor加载的,当把类名修改为ExceptionSonfig的时候,是在之后加载的。

当ExceptionProcessor去注入ExceptionMethodPool的时候会触发Bean的加载,这个时候bean容器里面找不到ExceptionProcessor,beandefinition容器里面也找不到,这个时候就抛出了异常。

如何解决这个bug

方法一

通过@DependsOn注解,让ExceptionProcessor在后面加载,实际上是当加载到ExceptionProcessor时触发getBean,让exceptionSonfig先加载

方法二

在注入 ExceptionMethodPool 之前先注入 ExceptionSonfig 让 ExceptionSonfig 先加载,不过不推荐这么玩儿,因为可能你的小伙伴看见你这个属性没有用到,直接给你删了

反正不管用啥骚操作,让ExceptionSonfig优先于ExceptionProcessor加载就行了。

最后总结一下bean加载的顺序

  1. 首先BeanPostProcessor已经各种内建bean优先加载
  2. 被依赖的bean优先加载
  3. 其余的按照扫描包的顺序加载
  4. 同一个包中,由于返回的是一个Set,所以顺序和类名相关

原创不易,如果对你又帮助,麻烦点个赞,谢谢!

我会不定期分享一些有意思的技术,点个关注不迷路-。 -

分类:
后端
标签:
分类:
后端
标签: