最近新接手一个项目,在日常环境启动的时候报错启动不了,查看日志发现是由于@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; } }
启动应用
可以看到启动报错,这里的env取值为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(); }
接着往里面跟一下代码
这里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; } }
咱们再来启动
这里已经能够获取到配置的变量 env=daily了,应用也成功启动了
如果你有更好的解决方法,欢迎一起探讨
参考:Spring源码之@Configuration原理 - 曹自标 - 博客园