Spring 初始化 bean 的循环依赖,导致InitializingBean 可能存在的空指针风险

提出问题

在 Spring 的框架中开发,当我们想在服务器启动后,就立马执行一个方法时,用的比较多的就是 implements InitializingBean 然后在 afterPropertiesSet 中做我们想做的事情,下面为示例代码:

public class UserServiceImpl implements UserService, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private MessageService messageService;

    // region spring

    /**
     * 初始化的节点 {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons()}
     */
    @Override
    public void afterPropertiesSet() {
        work();
    }

但是这么做,是有风险的,当两个Bean 出现循环依赖的时候,此时使用 InitializingBean 就会出现当前类中的属性被初始化,但是属性中依赖的属性未被初始化。
下面详细的介绍下我所遇到的问题。

示例

介绍问题

  • 首先有两个服务 MessageService,UserService 然后在其对应的实现类中互相依赖对方。当我们在 UserServiceImpl 中的 afterPropertiesSet() 中去通过 messageService 引用 userService 属性的时候,会发现 MessageServiceImpl 中的 userService 属性为 NULL
  • 代码
public interface MessageService {

    String getMessage();

    String getMessage(String msg);

    boolean checkUserNotNull();

}

@Component
public class MessageServiceImpl implements MessageService, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(MessageServiceImpl.class);

    @Resource
    private UserService userService;

    // region spring

    /**
     * 初始化的节点 {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons()}
     */
    @Override
    public void afterPropertiesSet() {

        LOGGER.info("init-MessageServiceImpl-afterPropertiesSet-user:{}", userService);
//        LOGGER.info("userNotNull:{}", checkUserNotNull());
    }

    // endregion spring

    // region public

    @Override
    public String getMessage() {
        return "hello world";
    }

    @Override
    public String getMessage(String msg) {
        return String.format("test-%s", msg);
    }

    @Override
    public boolean checkUserNotNull() {
        return userService != null;
    }

    // endregion public
}


public interface UserService {

    String getUser();

    String getUser(String msg);

}

@Component
public class UserServiceImpl implements UserService, InitializingBean{

    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private MessageService messageService;

    // region spring

    /**
     * 初始化的节点 {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons()}
     */
    @Override
    public void afterPropertiesSet() {

        LOGGER.info("init-UserServiceImpl-afterPropertiesSet-message:{}", messageService);
        LOGGER.info("afterPropertiesSet-userNotNull:{}", messageService.checkUserNotNull());

    }

    // endregion spring

    // region public

    @Override
    public String getUser() {
        return "hello world";
    }

    @Override
    public String getUser(String msg) {
        return String.format("test-%s", msg);
    }

    // endregion public
}

谈谈我的理解

在 spring 初始化bean 的时候。在 org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization 会去实例化所有的 bean,接着org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String) ,org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(),然后在
在 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean 中
这里有关键的三步。
1: addSingletonFactory 是为了防止 spring 初始化的时候,如果出现了循环依赖而导致的无限递归。但恰恰也就是这一步导致了上述的问题。
2: 给当前bean 的属性填充实例。
3: 这个步骤中会检测:如果实例实现了 InitializingBean 则调用对应的 afterPropertiesSet() 方法。

image_doCreateBean.png

我们来介绍下第一步,当准备初始化当前 messageService 的时候,会将当前bean 进行一些操作


    /**
     * Add the given singleton factory for building the specified singleton
     * if necessary.
     * <p>To be called for eager registration of singletons, e.g. to be able to
     * resolve circular references.
     * @param beanName the name of the bean
     * @param singletonFactory the factory for the singleton object
     */
    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(singletonFactory, "Singleton factory must not be null");
        synchronized (this.singletonObjects) {
            if (!this.singletonObjects.containsKey(beanName)) {
                this.singletonFactories.put(beanName, singletonFactory);
                this.earlySingletonObjects.remove(beanName);
                this.registeredSingletons.add(beanName);
            }
        }
    }

接着走第二步 populateBean() , 在postProcessPropertyValues 中会去实例化当前类中的属性,也就是 userService。然后在 postProcessPropertyValues inject 所有的属性。


// org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean
        boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
        boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

        if (hasInstAwareBpps || needsDepCheck) {
            PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
            if (hasInstAwareBpps) {
                for (BeanPostProcessor bp : getBeanPostProcessors()) {
                    if (bp instanceof InstantiationAwareBeanPostProcessor) {
                        InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                        pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
                        if (pvs == null) {
                            return;
                        }
                    }
                }
            }
            if (needsDepCheck) {
                checkDependencies(beanName, mbd, filteredPds, pvs);
            }
        }

// org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessPropertyValues
    @Override
    public PropertyValues postProcessPropertyValues(
            PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {

        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }

// org.springframework.beans.factory.annotation.InjectionMetadata.InjectedElement#inject
    public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable {
        Collection<InjectedElement> elementsToIterate =
                (this.checkedElements != null ? this.checkedElements : this.injectedElements);
        if (!elementsToIterate.isEmpty()) {
            boolean debug = logger.isDebugEnabled();
            for (InjectedElement element : elementsToIterate) {
                if (debug) {
                    logger.debug("Processing injected element of bean '" + beanName + "': " + element);
                }
                element.inject(target, beanName, pvs);
            }
        }
    }


        /**
         * Either this or {@link #getResourceToInject} needs to be overridden.
         */
        protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
            if (this.isField) {
                Field field = (Field) this.member;
                ReflectionUtils.makeAccessible(field);
                field.set(target, getResourceToInject(target, requestingBeanName));
            }
            else {
                if (checkPropertySkipping(pvs)) {
                    return;
                }
                try {
                    Method method = (Method) this.member;
                    ReflectionUtils.makeAccessible(method);
                    method.invoke(target, getResourceToInject(target, requestingBeanName));
                }
                catch (InvocationTargetException ex) {
                    throw ex.getTargetException();
                }
            }
        }

我们可以注意到上面的 getResourceToInject() 这里尝试从 org.springframework.context.support.AbstractApplicationContext#getBean() 中拿到 userService 这么一个实例,然后就会走到 org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean,org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(),org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean,最后又绕回到上图 image_doCreateBean.png,只不过这次是尝试 实例化 userService。当执行到 populateBean() 的时候,很容易想到,这个时候又会去尝试实例化 UserServiceImpl 中的 messageService 属性。然后又会走到 org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean ,尝试拿到 messageService 实例,可以看到在 doGetBean 中执行到 getSingleton(),而getSingleton 会尝试从 singletonFactories 这个一个属性中取得是否存在对应的实例,如果有则返回。而我们回到上面的 addSingletonFactory(),在执行这个方法的时候,已经将 messageService put 到了 map中。而此时这个messageService 是没有userService属性的,因为初始化 messageService 的栈还没结束。
紧接着 userService 拿到了不完整的 messageService 走完了 populateBean(),接着就去执行了 initializeBean() 方法,也就是会走到 UserServiceImpl 中的 afterPropertiesSet(),而这个时候 userService 中 messageService 的 userService 是不存在的,所以一切就明了了。
可以在 image_initializeBean_user.png 中看到 doCreateBean[1] 代表 实例化 messageService 的过程还未出栈,doCreateBean[2] 表示正在实例化 userService。我们可以看到在进入 initializeBean() 的过程中, UserServiceImpl 中的 messageService 的 userService 为 NULL

       // org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
    @SuppressWarnings("unchecked")
    protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {

        final String beanName = transformedBeanName(name);
        Object bean;

        // Eagerly check singleton cache for manually registered singletons.
        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) {
            if (logger.isDebugEnabled()) {
                if (isSingletonCurrentlyInCreation(beanName)) {
                    logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
                            "' that is not fully initialized yet - a consequence of a circular reference");
                }
                else {
                    logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
                }
            }
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        }
  ...
  }

        // org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
image_initializeBean_user.png

更好的办法

  • 为了解决这种可能存在的 NPE,可以使用 implements ApplicationListener 来解决,在onApplicationEvent() 中是不会存在这种情况的。因为 onApplicationEvent 实在 finishRefresh() 中执行,而此时已经执行过 finishBeanFactoryInitialization() 容器内所有的bean 都初始化完成了。
@Component
public class UserServiceImpl implements UserService, InitializingBean, ApplicationListener<ContextRefreshedEvent> {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private MessageService messageService;

    // region spring

    /**
     * 初始化的节点 {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#preInstantiateSingletons()}
     */
    @Override
    public void afterPropertiesSet() {

        LOGGER.info("init-UserServiceImpl-afterPropertiesSet-message:{}", messageService);
        LOGGER.info("afterPropertiesSet-userNotNull:{}", messageService.checkUserNotNull());

    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {

        ApplicationContext applicationContext = event.getApplicationContext();

        LOGGER.info("onApplicationEvent-userNotNull:{}", messageService.checkUserNotNull());
    }
}
image_refresh.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344