SpringBoot那些注入不了的占位符@Value(${xx})

616 阅读6分钟

一、背景

1.1 Spring中的@Value注解

在Spring中,一般在application.properties 文件中指定配置项的值

spring.demo.name=spring-demo-app-name

然后使用@Value +占位符的方式来注入配置项,Spring就能自动把这个值注入进来:

@Configuration  
public class DemoConfiguration {  
  
    @Value("${spring.demo.name}")  
    private String name;
    
	@Bean  
	public String demoName() {
		// spring启动了以后,这里输出的就是 spring-demo-app-name
	    System.out.println(name);  
	    return name;  
	}
}

1.2 @Value无法注入

Spring的这个@Value注解的使用是比较简单的,一般很少使用出错。但是我们却发现在某次代码重构以后,标记了@Value注解的字段无法注入配置项的值了,而且并不是所有的配置项都无法注入,而是只有某一个类里的字段值无法用@Value注入。这个类的代码如下:

@Configuration  
public class MybatisConfiguration {  
  
    @Value("${mapper.package}")  
    private String basePackage;
  
    @Bean  
    public MapperScannerConfigure mapperScannerConfigure() {  
        MapperScannerConfigure mapperScannerConfigure = new MapperScannerConfigure();
        // 运行起来的时候,这个this.basePackage得到的值是null 
        mapperScannerConfigure.setBasePackage(this.basePackage);
        // xxxxx 这里省略一些无关的代码
        return mapperScannerConfigure; 
    }
}

application.properties文件中也有对应的配置:

mapper.package=com.xxxx.xxx.xxx

使用方式肯定是没错的,毕竟项目里其他地方使用@Value也正常注入了配置项的值。那就没办法了,只能研究下Spring的源码了,看看Spring是怎么把配置项注入进来的。

二、@Value配置项注入的原理

先debug了下能正常注入配置项字段的Bean,发现通过@Value标记的字段,是在Spring的org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties 这个方法中处理的:

@Override  
public PropertyValues postProcessProperties(PropertyValues pvs, 
	Object bean, String beanName) {
	// Step1. 第一步找到这个bean的metadata
   InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);  
   try {  
	   // Step2. 第二步直接执行注入,会把配置项的值通过反射的方式设置为字段的值
      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;  
}

2.1 findAutowiringMetadata找到被@Value标记的字段

先来看一下这个findAutowiringMetadata方法,从名字上来看跟@Value毫无关系,他的作用其实是去扫描这个Bean当中所有被@Autowired@Value标记的字段。

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {  
   // Fall back to class name as cache key, for backwards compatibility with custom callers.  
   String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());  
   // Quick check on the concurrent map first, with minimal locking.
   // 这里是个缓存,不需要关心,直接看后面构造  InjectionMetadata 的地方
   InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);  
   if (InjectionMetadata.needsRefresh(metadata, clazz)) {  
	   synchronized (this.injectionMetadataCache) {  
		   metadata = this.injectionMetadataCache.get(cacheKey);  
	         if (InjectionMetadata.needsRefresh(metadata, clazz)) {  
             if (metadata != null) {  
               metadata.clear(pvs);  
             }
	         // 这个方法里构造出 InjectionMetadata           
	         metadata = buildAutowiringMetadata(clazz);  
	         this.injectionMetadataCache.put(cacheKey, metadata);  
			}     
		}   
	}   
    return metadata;  
}

private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {  
   do {  
      final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();  
      // ... 中间省略一些无关的代码
      ReflectionUtils.doWithLocalFields(targetClass, field -> {
		//可以看到这里是通过反射来拿所有的字段,而这个字段的过滤条件则是这个
		// findAutowiredAnnotation方法  
         MergedAnnotation<?> ann = findAutowiredAnnotation(field);
    } 
    // ... 中间省略一些无关的代码
   }   while (targetClass != null && targetClass != Object.class);  
   return InjectionMetadata.forElements(elements, clazz);  
}
@Nullable  
private MergedAnnotation<?> findAutowiredAnnotation(AccessibleObject ao) {  
   MergedAnnotations annotations = MergedAnnotations.from(ao);
   // 可以看到,最主要是通过这个 this.autowiredAnnotationTypes 来进行过滤的
   // 实际上这是个List,里面的元素有两个,分别是Autowired.class 和 Value.class
   // 所以这个方法能找到所有用@Autowired和@Value标记的字段
   for (Class<? extends Annotation> type : this.autowiredAnnotationTypes) {  
      MergedAnnotation<?> annotation = annotations.get(type);  
      if (annotation.isPresent()) {  
         return annotation;  
      }   }   return null;  
}
// 而 this.autowiredAnnotationTypes这个列表里的元素是在构造函数里加进去的
@SuppressWarnings("unchecked")  
public AutowiredAnnotationBeanPostProcessor() {
   // 看到这里谜题就解开了,AutowiredAnnotationBeanPostProcessor 确实能找到@Value标记的字段
   this.autowiredAnnotationTypes.add(Autowired.class);  
   this.autowiredAnnotationTypes.add(Value.class);
   // ... 中间省略一些无关的代码
}										

2.2 通过反射给字段赋值

看这个方法:org.springframework.beans.factory.annotation.InjectionMetadata#inject

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {  
   Collection<InjectedElement> checkedElements = this.checkedElements;  
   Collection<InjectedElement> elementsToIterate =  
         (checkedElements != null ? checkedElements : this.injectedElements);  
   if (!elementsToIterate.isEmpty()) {  
      for (InjectedElement element : elementsToIterate) {
	     // 这里会遍历每个InjectedElement做处理,然后我们InjectedElement有两个实现类
	     // 分别是AutowiredFieldElement和AutowiredMethodElement
	     // 我们这里是注入字段,所以就直接看AutowiredFieldElement的inject方法即可
         element.inject(target, beanName, pvs);  
      }   
    }
}
// org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject 
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {  
   Field field = (Field) this.member;  
   Object value;  
   if (this.cached) {  
      try {  
         value = resolvedCachedArgument(beanName, this.cachedFieldValue);  
      } catch (NoSuchBeanDefinitionException ex) {  
         // Unexpected removal of target bean for cached argument -> re-resolve  
         value = resolveFieldValue(field, bean, beanName);  
      }
    } else {  
      value = resolveFieldValue(field, bean, beanName);  
   }
   if (value != null) {
	  // 这里直接通过反射赋值,就完成了配置项注入到被@Value标记的字段上的任务  
      ReflectionUtils.makeAccessible(field);  
      field.set(bean, value);  
   }
}

三、提前初始化的Bean

了解了原理之后,我们开始研究为什么MybatisConfiguration里用@Value标记的字段没有被注入配置项的值,所以我们在org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties 这个方法上打了条件断点,但是发现并没有走进来。也就是说,这个Bean的处理并没有经过AutowiredAnnotationBeanPostProcessor处理,这就奇怪了。

那么就干脆把断点放在最前面,从org.springframework.context.support.AbstractApplicationContext#refresh开始一步步debug下来,发现是通过走到org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean方法里,再去调用AutowiredAnnotationBeanPostProcessor的。从堆栈上来看,能正常注入@Value字段值的Bean和不能正常注入@Value字段值的Bean,进入这个populateBean方法的时机是不一样的:

public void refresh() throws BeansException, IllegalStateException {  
   synchronized (this.startupShutdownMonitor) {  
      StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); 
      // .....
      try {  
         // .....
         // MybaticConfiguration 从这个地方进入到populateBean方法
         // 此时还没有注册AutowiredAnnotationBeanPostProcessor这个processor
         // 那么pupulateBean方法中自然无法调用AutowiredAnnotationBeanPostProcessor来执行
         invokeBeanFactoryPostProcessors(beanFactory);  
  
         // 这个地方才会把AutowiredAnnotationBeanPostProcessor注册进去
         registerBeanPostProcessors(beanFactory);
         // .....
         
         // 能正常注入@Value字段值的Bean从这个地方为入口进入到populateBean方法
         // 此时pupulateBean方法中可以调用到AutowiredAnnotationBeanPostProcessor来执行
         finishBeanFactoryInitialization(beanFactory);  
         // .....
      }  catch (BeansException ex) {}
}

看到这里,自然的就会产生两个疑问:

  • 为什么MybaticConfiguration执行populateBean方法更早?
  • MybaticConfigurationfinishBeanFactoryInitialization方法里不会再次执行populateBean方法么?再执行一遍就能调用到AutowiredAnnotationBeanPostProcessor然后完成字段值的注入了么?

先回答第二个问题:继续翻看源码就会发现populateBean是通过org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean方法为入口执行进来的,这是个创建bean的方法,肯定不可能执行两遍的。

也就是说,能正常注入@Value字段值的Bean是在执行finishBeanFactoryInitialization方法才创建出来的。这时候第一个问题其实就变成了,MybaticConfiguration这个Bean为什么会提前被创建??

再继续从invokeBeanFactoryPostProcessors方法往下翻,就会有结论:

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
   // 从这个方法进去
   PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());  
  
   //.....
   }
}
public static void invokeBeanFactoryPostProcessors(  
      ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {  
      // .....
      // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.  
      boolean reiterate = true;  
      while (reiterate) {  
         reiterate = false;
         // 在这里,去拿到所有的BeanDefinitionRegistryPostProcessor.class bean
         // 结果发现,拿到了我们在MybaticConfiguration里定义的那个bean MapperScannerConfigure
         // 翻看了一下这个类的定义,确实是实现了BeanDefinitionRegistryPostProcessor
         postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);  
         for (String ppName : postProcessorNames) {  
            if (!processedBeans.contains(ppName)) {
               // 然后开始创建实现了BeanDefinitionRegistryPostProcessor的这些Bean
               // 自然也包括MapperScannerConfigure,由于这个Bean是被定义在
               // MybaticConfiguration里的
               // 所以Spring就先创建了MybaticConfiguration这个Bean
               // 这就解释了MybaticConfiguration被提前创建的原因
               currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
            }         
        }         
      }  

}

看到这里,这个问题的原因就很明显了,因为MapperScannerConfigure实现了BeanDefinitionRegistryPostProcessor,它需要被提前创建,所以定义这个Bean的那个ConfigurationBean(MybaticConfiguration)也需要被提前创建。而这个提前创建的时机是比较早的,此时AutowiredAnnotationBeanPostProcessor并没有被注册进来,所以执行不到,也就导致MybaticConfiguration里标记了@Value的字段无法被注入配置项的值。

四、解决方案

既然发现了问题,要怎么解决呢??其实方案很简单,回头去看org.springframework.context.support.AbstractApplicationContext#refresh方法,可以发现其实在prepareRefresh方法里,已经把Environment创建好了。对于SpringBoot应用来说,这个Environment其实创建和初始化的时机更早。

也就是说,在创建MybatisConfiguration这个Bean之前,Environment已经处理好了,那么可以手动调用Environment来读配置项的值,代码修改如下:

@Configuration  
public class MybatisConfiguration implements EnvironmentAware {

	private Environment environment;

	@Override  
	public void setEnvironment(Environment environment) {  
	    this.environment = environment;  
	}
  
    @Bean  
    public MapperScannerConfigure mapperScannerConfigure() {  
        MapperScannerConfigure mapperScannerConfigure = new MapperScannerConfigure();
        // 从environment里可以直接取到这个配置项的值
        String basePackage = this.environment.resolvePlaceholders("${mapper.package}");
        mapperScannerConfigure.setBasePackage(basePackage);
        // xxxxx 这里省略一些无关的代码
        return mapperScannerConfigure; 
    }
}