@Value为null问题排查及解决方法

最近新接手一个项目,在日常环境启动的时候报错启动不了,查看日志发现是由于@Value的值为null,导致启动报错

我们先来还原一下事故现场

自定义一个BeanDefinitionRegistryPostProcessor来模拟Mybatis的MapperScannerConfigurer

public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    private ConsoleApi consoleApi;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        System.out.println("this is MyBeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry");

        System.out.println("MyBeanDefinitionRegistryPostProcessor: "+ consoleApi);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)
        throws BeansException {
        System.out.println("this is MyBeanDefinitionRegistryPostProcessor#postProcessBeanFactory");
    }

    public ConsoleApi getDivisionDO() {
        return consoleApi;
    }

    public void setDivisionDO(ConsoleApi consoleApi) {
        this.consoleApi = consoleApi;
    }
}

在配置类中注入这个Bean

@Configuration
public class DsConfig  {

    // 这里通过@Value注入配置信息
    @Value("spring.demo.hsf.env")
    private String env;

   
    @Bean
    public ConsoleApi divisionDO(){
        System.out.println("DsConfig env ===========>>>> " + env);
        if("daily".equalsIgnoreCase(env)){
            return new ConsoleApi();
        }else {
            throw new RuntimeException("环境不支持");
        }
    }

    @Bean
    public MyBeanDefinitionRegistryPostProcessor myBeanDefinitionRegistryPostProcessor(ConsoleApi ConsoleApi){
        MyBeanDefinitionRegistryPostProcessor myBeanDefinitionRegistryPostProcessor = new MyBeanDefinitionRegistryPostProcessor();
        myBeanDefinitionRegistryPostProcessor.setDivisionDO(ConsoleApi);
        return myBeanDefinitionRegistryPostProcessor;
    }
}

启动应用

@Value为null问题排查及解决方法

可以看到启动报错,这里的env取值为null

@Value为null问题排查及解决方法

 

很是疑惑,这里我明明配置了env=daily,为啥取不到值呢?

然后开始面向百度编程.......

首先我将自定义个这个BeanDefinitionRegistryPostProcessor注释掉,发现应用能够成功启动,那么问题十有八九就是这个自定义的BeanDefinitionRegistryPostProcessor导致的

BeanDefinitionRegistryPostProcessor是一个BeanFactoryPostProcessor 那么这个Bean是在什么时候被示例化的呢,咱们来debug一下

Spring容器启动

org.springframework.context.support.AbstractApplicationContext#refresh

{
   // Allows post-processing of the bean factory in context subclasses.
   postProcessBeanFactory(beanFactory);

   // Invoke factory processors registered as beans in the context.
   // 这里调用Spring容器的BeanFactoryPostProcessor 
   // 注意这里调用的时候还没有初始化自定义的BeanFactoryPostProcessor,而是创建BeanFactory的是Spring 自己New出来的三个BeanFactoryPostProcessor
   invokeBeanFactoryPostProcessors(beanFactory);

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

   // Initialize message source for this context.
   initMessageSource();

   // Initialize event multicaster for this context.
   initApplicationEventMulticaster();

   // Initialize other special beans in specific context subclasses.
   onRefresh();

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

   // Instantiate all remaining (non-lazy-init) singletons.
   finishBeanFactoryInitialization(beanFactory);

   // Last step: publish corresponding event.
   finishRefresh();
}

接着往里面跟一下代码

 @Value为null问题排查及解决方法

这里Spring会两次调用获取BeanDefinitionRegistryPostProcessor类型的Bean名称并实例化Bean

第一次调用获取到了是框架自身的ConfigurationClassPostProcessor 加载应用中所有的BeanDefinition

第二次调用是获取自定义的BeanDefinitionRegistryPostProcessor并实例化这些Bean,就会通过代理的方式处理@Bean注入的Bean,这个时候其实Spring还没有初始化其他的Bean包括DsConfig所以这个时候@Value还没有被处理,那么env的值当然为nulll了

现在问题找到了,那么该怎么去解决呢?

问题的关键是在实例化MyBeanDefinitionRegistryPostProcessor的时候,DsConfig还没有被实例化出来,那能不能在实例化MyBeanDefinitionRegistryPostProcessor之前就把DsConfig给实例化出来呢?顺着这思路我给出了下面的解决方法

@Configuration
public class DsConfig implements ApplicationContextAware, InitializingBean {

    // 这里通过@Value注入配置信息
    //@Value("${spring.demo.hsf.env}")
    private String env;

    private ApplicationContext applicationContext;

    @Bean
    public ConsoleApi consoleApi(DsConfig dsConfig) {
        System.out.println("DsConfig env ===========>>>> " + env);
        if ("daily".equalsIgnoreCase(env)) {
            return new ConsoleApi();
        } else {
            throw new RuntimeException("环境不支持");
        }
    }

    @Bean
    public MyBeanDefinitionRegistryPostProcessor myBeanDefinitionRegistryPostProcessor(ConsoleApi ConsoleApi){
        MyBeanDefinitionRegistryPostProcessor myBeanDefinitionRegistryPostProcessor = new MyBeanDefinitionRegistryPostProcessor();
        myBeanDefinitionRegistryPostProcessor.setDivisionDO(ConsoleApi);
        return myBeanDefinitionRegistryPostProcessor;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Environment environment = applicationContext.getEnvironment();
        String env = environment.getProperty("spring.demo.hsf.env");
        this.env = env;

    }

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

咱们再来启动

@Value为null问题排查及解决方法

 这里已经能够获取到配置的变量 env=daily了,应用也成功启动了 

如果你有更好的解决方法,欢迎一起探讨

参考:Spring源码之@Configuration原理 - 曹自标 - 博客园

 

上一篇:【无标题】


下一篇:emcc