案例

最近在分析一个有关类加载过程的问题,代码如下:

@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    /**
     * 根据类型获取bean
     */
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

}

这是一个Spring上下文的工具类,相信大家都用过这样的工具,当我们需要在一个非Spring管理的类中调用Spring管理的bean时,就需要这样一个工具类。本次分析的案例就是这样的应用场景,需要在工具类中使用Spring管理的bean:

/**
 * 测试bean
 */
@Service
public class TestBean {

    public TestBean() {
        System.err.println("TestBean init");
    }

    public void test() {
        System.err.println("test invoke");
    }

    @Override
    public String toString() {
        return "TestBean toString";
    }
}
/**
 * 测试工具类
 */
public class TestUtils {
    
    public static TestBean testBean = SpringContextUtils.getBean(TestBean.class);

    public static void test() {
        testBean.test();
    }
    
}

这里不免有一个疑问,这里TestUtils.testBean静态变量初始化需要依赖Spring的上下文环境对象,而Spring上下文环境对象是在SpringContextUtils bean创建时赋值的,那么会不会有些场景可能我们在SpringContextUtils bean创建之前就使用了TestUtils.testBean静态变量或者调用了TestUtils.test静态方法,这时不就有问题了么?

在分析之前我们需要一些基础的知识准备:

分析

根据JDK的类加载过程,我们知道触发类初始化的场景大致有以下几种:

  • new关键字实例化对象的时候
  • 读取或设置一个类的静态字段(非常量)的时候
  • 调用一个类的静态方法的时候
  • 使用java.lang.reflect包的方法对类进行反射调用的时候
  • 虚拟机会先初始化含有main方法主类
  • 使用JDK1.7的动态语言支持时

Spring Aware的作用这里就不再重复了,在Spring bean创建的过程中,在bean属性填充完成之后,会调用Aware的相关set方法设置相关资源,这里的ApplicationContextAware接口的setApplicationContext方法也就是在这个时候调用的。

根据以上的知识准备,我们来验证几个猜测:

场景一

我们猜测在Spring上下文初始化之前初始化TestUtils一定会报错:

@SpringBootApplication
@PropertySource(value = {"application.properties"})
@ImportResource(locations = {"classpath*:spring/spring-*.xml"})
public class BootApplication {

    public static void main(String[] args) {
        TestUtils.test();
        SpringApplication.run(BootApplication.class, args);
    }
}

启动运行之后,果然发现在运行到TestUtils.test()这行代码时报了NPE(java.lang.NullPointerException),原因很简单,在初始化TestUtils时,初始化TestBean类型的静态属性需要去调用SpringContextUtils.getBean方法,这时因为Spring上下文还没有开始初始化,所以这时applicationContext为null,调用时自然会出现NPE。

场景二

下面为整个Spring上下文初始化时的一些主要步骤,其中,非lazy-init的单例bean的初始化是在第8步,也就是SpringContextUtils bean的初始化就在这一步:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
    	// 为刷新做准备工作
        prepareRefresh();
        // 初始化BeanFactory,并解析配置
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        // 为BeanFactory填充功能
        prepareBeanFactory(beanFactory);
        try {
            // 1、子类扩展对BeanFactory进行额外处理
            postProcessBeanFactory(beanFactory);
            // 2、调用注册的BeanFactoryPostProcessors的postProcessBeanFactory方法
            invokeBeanFactoryPostProcessors(beanFactory);
            // 3、注册BeanPostProcessors
            registerBeanPostProcessors(beanFactory);
            // 4、初始化MessageSource,用于国际化处理
            initMessageSource();
            // 5、初始化应用事件广播器
            initApplicationEventMulticaster();
            // 6、子类扩展初始化一些个性化的bean
            onRefresh();
            // 7、找到ApplicationListener bean,并注册
            registerListeners();
            // 8、初始化剩下的所有非lazy-init的单例bean
            finishBeanFactoryInitialization(beanFactory);
            // 9、初始化LifecycleProcessor(生命周期处理器),发步对应的事件通知,如果配置了JMX,则注册MBean
            finishRefresh();
        } catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
            }
            // 销毁已经创建的单例Bean
            destroyBeans();
            // 重置active标记为false
            cancelRefresh(ex);
            throw ex;
        } finally {
            resetCommonCaches();
        }
    }
}

根据上面的初始化顺序我们猜测在SpringContextUtils bean初始化之前初始化TestUtils一定会报错,测试方式我们注册一个自定义的BeanDefinitionRegistryPostProcessor,BeanDefinitionRegistryPostProcessor允许我们注册自定义的BeanDefinition,例如我们在使用Spring整合Mybatis的时候会配置一个类MapperScannerConfigurer,用来扫描我们工程中定义的所有Mapper接口,MapperScannerConfigurer就实现了BeanDefinitionRegistryPostProcessor,而BeanDefinitionRegistryPostProcessor的调用是在第2步:

@Component
public class TestBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        TestUtils.test();
    }
}

启动运行之后,果然发现在运行到postProcessBeanDefinitionRegistry方法时,抛出了NPE,因为这里调用了TestUtils的静态方法,所以会初始化TestUtils,这时虽然Spring上下文正在初始化中,但是SpringContextUtils bean还没有初始化,调用时自然会出现NPE。

包括spring boot在bean初始化之前的一些扩展,我们在使用的时候都要注意这一点,同样都会有问题。

场景三

我们把TestUtils的调用放在main方法的最后一行:

@SpringBootApplication
@PropertySource(value = {"application.properties"})
@ImportResource(locations = {"classpath*:spring/spring-*.xml"})
public class BootApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
        TestUtils.test();
    }
}

再次启动,这次应用正常启动,控制台也正常打印出"TestBean init"和"test invoke",这里在调用TestUtils的静态方法时,Spring应用上下文已经完成了初始化,SpringContextUtils bean也完成了初始化,所以这里的调用是没问题的。

场景四

上面我们看到,Spring上下文会初始化非lazy-init的单例bean,那么如果SpringContextUtils是lazy-init的bean会怎样呢?我们来试一下,在SpringContextUtils上加上@Lazy注解,使之变成懒加载的bean:

@Component
@Lazy
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    /**
     * 根据类型获取bean
     */
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

}

再次启动,熟悉的NPE又出现了,每日三空的魔咒今日又应验了,这里因为SpringContextUtils是懒加载的bean,懒加载的bean只有在真正的被依赖时才会初始化,调用SpringContextUtils.getBean方法并不会触发bean的初始化,所以调用时自然会出现NPE。

这种情况只有用Spring IOC依赖注入的方式去依赖SpringContextUtils才会触发SpringContextUtils bean的初始化,但是这种依赖的方式就失去了SpringContextUtils这个类的意义了,我们就是要以静态调用的方式去获取Spring的bean,所以才会使用它,所以SpringContextUtils绝对不要设置成懒加载。

总结

根据以上几个测试场景,我们得出以下结论:

  1. 在Spring上下文初始化之前,不要依赖Spring的bean,这其实是个废话,但是日常开发过程中,我们还是要注意这一点
  2. 在Spring初始化非lazy-init的单例bean完成之前,不要依赖这些未初始化完成的bean,这里需要我们熟知各种扩展的触发时间点
  3. 类似示例中SpringContextUtils这种需要以静态方式获取Spring bean的类,不要设置为lazy-init

以上这几个注意的点在大部分情况下我们不会遇到,因为我们日常开发中大部分调用的场景都是在应用启动完成之后触发,这时未做特殊处理的资源基本上都已经加载完成,所以不会报错,但是也难免我们会涉及到在应用启动过程中会触发的调用场景,例如我们对外提供的一些client包等,多多注意,避免采坑。