“@Value 注入失敗”引發的一系列騷操作


背景

項目里想用@Value注入一個字段,可沒想到怎么都注入不成功,但換另一種方式就可以,於是就想了解一下@Value注解不成功的原因。

本文的代碼是基於Spring的5.3.8版本

模擬@Value成功的場景

首先為了搞清楚@Value注解不成功的原理,我們先用最簡單的代碼模擬一下它注入成功的例子:

在resources文件夾下定義了application.yml,內容如下:
my:
  value: hello
定義一個配置類:
@Configuration
@Data
public class Config {
    @Value("${my.value}")
    private String myValue;
}
定義一個測試類:
public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        Config config = context.getBean(Config.class);
        System.out.println(config);
    }
}
輸出:
Config(myValue=${my.value})

上面的代碼做了幾件事情:

  1. resources/application.yml文件中定義了my.value=hello
  2. 定義了一個Config類,利用@value注解將hello注入到字段myValue
  3. 定義了一個Main類測試效果

測試類做了幾件事情:

  1. 使用AnnotationConfigApplicationContext這個容器加載配置類
  2. 獲取配置類Config
  3. 輸出注入的字段myValue

從結果來看,並沒有注入成功,我的第一感覺就是沒有把我們的application.yml文件里的內容加載到environment里面,那我們就來看看environment里面都有什么內容,如下代碼:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        ConfigurableEnvironment environment = context.getEnvironment();
        System.out.println(environment);
    }
}

從結果來看:

  1. environment並沒有包含我們application.yml文件里的內容
  2. 但它包含了其他兩個東西,分別是systemPropertiessystemEnvironment

那我們就需要把application.yml文件里的內容加載到environment,需要考慮以下兩個問題:

  1. 怎么解析yml文件的內容
  2. 怎么把解析的內容放到environment

針對問題一:可以利用spring自帶的YamlPropertySourceLoader這個類的load()方法,它會返回一個List<PropertySource<?>>

針對問題二:我們可以先來看一下默認的內容是怎么放進去的,看一下getEnvironment()的源碼:

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
	public ConfigurableEnvironment getEnvironment() {
		if (this.environment == null) {
			this.environment = createEnvironment();
		}
		return this.environment;
	}
	protected ConfigurableEnvironment createEnvironment() {
		return new StandardEnvironment();
	}
} 

從上面可以看出默認創建的是一個StandardEnvironment,我們再來看一下它的初始化:

public class StandardEnvironment extends AbstractEnvironment {
	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

	@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(
				new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
		propertySources.addLast(
				new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
	}
}
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
	public AbstractEnvironment() {
		this(new MutablePropertySources());
	}

	protected AbstractEnvironment(MutablePropertySources propertySources) {
		this.propertySources = propertySources;
		this.propertyResolver = createPropertyResolver(propertySources);
		customizePropertySources(propertySources);
	}
}

從上面代碼可以看出,在StandardEnvironment.customizePropertySources()的方法中,是通過propertySources.addLast()方法添加進去的,那我們可以照葫蘆畫瓢,如下:

public class Main {
    public static void main(String[] args) throws IOException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        ConfigurableEnvironment environment = context.getEnvironment();
        System.out.println(environment);
        YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
        List<PropertySource<?>> propertySources = loader.load("my-properties",
                new FileSystemResource("/Users/xxx/spring-boot-study/src/main/resources/application.yml"));
        environment.getPropertySources().addLast(propertySources.get(0));
        System.out.println(environment);
    }
}

從上面結果可以看出,我們已經成功把我們的application.yml文件內容放到environment中了

那我們把測試代碼改成:

public class Main {
    public static void main(String[] args) throws IOException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
        List<PropertySource<?>> propertySources = loader.load("my-properties",
                new FileSystemResource("/Users/xxx/spring-boot-study/src/main/resources/application.yml"));
        context.getEnvironment().getPropertySources().addLast(propertySources.get(0));
        Config config = context.getBean(Config.class);
        System.out.println(config);
    }
}

輸出:
Config(myValue=${my.value})

從上面的結果可以看出,還是沒有得到我們想要的結果,這是因為conig類會提前初始化,是在refresh()方法中的finishBeanFactoryInitialization()方法進行的,所以我們要在這一步之前把我們的內容放到environment

翻了一翻refresh()這個方法,發現在prepareRefresh()這個方法里有一個initPropertySources()的方法,注釋寫着初始化一系列的資源,所以我們可以在這個方法里面加載我們的配置文件,於是變成:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
            @SneakyThrows
            @Override
            public void initPropertySources() {
                YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
                List<PropertySource<?>> propertySources = loader.load("my-properties",
                        new FileSystemResource("/Users/xxx/spring-boot-study/src/main/resources/application.yml"));
                getEnvironment().getPropertySources().addLast(propertySources.get(0));
            }
        };
        Config config = context.getBean(Config.class);
        System.out.println(config);
    }
}

輸出:
Config(myValue=hello)

到目前為止,我們模擬了@Value注入成功的場景,項目里面應該不會出現這種資源沒有加載的問題,因為這些事情spring boot都幫我們做好了

所以直接在@Configuration類下直接用@Value是沒有問題的

模擬注入不成功的場景

現在我們就來模擬一下注入不成功的場景,配置類改成如下:

@Configuration
@Data
public class Config {
    @Value("${my.value}")
    private String myValue;

    @Bean
    public MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
        return new MyBeanFactoryPostProcessor();
    }

    public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        }
    }
}

輸出結果:

Config(myValue=null)

這就是我項目上遇到的問題,在配置類中再生成一個BeanFactoryPostProcessor后,@Value就注入不成功了

但只要把這個方法寫成static就可以了,如下:

@Configuration
@Data
public class Config {
    @Value("${my.value}")
    private String myValue;

    @Bean
    public static MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
        return new MyBeanFactoryPostProcessor();
    }

    public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        }
    }
}

輸出結果:

Config(myValue=hello)

看看為什么沒有注入成功

@Value是由AutowiredAnnotationBeanPostProcessor.postProcessProperties()處理的,所以我們就以這里為入口進行調試。

我們先把static去掉:

發現沒有執行到上述方法,那我們再把static加上,看一下成功的情況:

可以看到,是可以到這個方法的,而且知道這個方法是被AbstractAutowireCapableBeanFactory.populateBean()調用的,我們再看一下這里的情況:

從上圖可以看出,getBeanPostProcessorCache().instantiationAware是有AutowiredAnnotationBeanPostProcessor這個實例的

那我們再來看一下不加static這里的情況:

果然,沒有注入成功的原因是在創建config實例的時候,還沒有創建AutowiredAnnotationBeanPostProcessor實例

我們來看一下這個getBeanPostProcessorCache().instantiationAware是什么東西,又是如何生成的

發現只有在AbstractBeanFactory.getBeanPostProcessorCache()這個方法會將InstantiationAwareBeanPostProcessor添加到instantiationAware,如下:

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
	BeanPostProcessorCache getBeanPostProcessorCache() {
		BeanPostProcessorCache bpCache = this.beanPostProcessorCache;
		if (bpCache == null) {
			bpCache = new BeanPostProcessorCache();
			for (BeanPostProcessor bp : this.beanPostProcessors) {
				if (bp instanceof InstantiationAwareBeanPostProcessor) {
					bpCache.instantiationAware.add((InstantiationAwareBeanPostProcessor) bp);
					if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
						bpCache.smartInstantiationAware.add((SmartInstantiationAwareBeanPostProcessor) bp);
					}
				}
				if (bp instanceof DestructionAwareBeanPostProcessor) {
					bpCache.destructionAware.add((DestructionAwareBeanPostProcessor) bp);
				}
				if (bp instanceof MergedBeanDefinitionPostProcessor) {
					bpCache.mergedDefinition.add((MergedBeanDefinitionPostProcessor) bp);
				}
			}
			this.beanPostProcessorCache = bpCache;
		}
		return bpCache;
	}
}

從上面的代碼看出,本質還是從this.beanPostProcessors獲取的,我們來看一下什么時候會把AutowiredAnnotationBeanPostProcessor添加到容器中,如下:

從上圖可知:AutowiredAnnotationBeanPostProcessor是在refresh()方法中的registerBeanPostProcessors()方法注入的

我們再來看一下加static方法的config類是什么時候加載的:

再來看一下不加static方法的config類是什么時候加載的

我們來總結一下提到的方法在refresh()方法中的順序:

invokeBeanFactoryPostProcessors(); ——> 不加static的時候,在這一步加載config類

registerBeanPostProcessors();  ——> 注冊AutowiredAnnotationBeanPostProcessor

finishBeanFactoryInitialization(); 加static的時候,在這一步加載config類

所以我們就知道原因了:當不加static字段時候,加載config類的時候,我們的AutowiredAnnotationBeanPostProcessor還沒有注冊,所以就會不成功,而當加上static后,我們加載config類的時候,我們的AutowiredAnnotationBeanPostProcessor已經注冊好了。

為什么加static和不加static的加載順序是不一樣的呢

spring容器會在invokeBeanFactoryPostProcessors()這一步會加載所有的BeanFactoryPostProcessor,如果用static修飾的話,則不會加載config類,反之會加載。原因如下:

上圖已經給出了原因,如果生成bean的工廠方法是static方法就不會加載,反之會加載。

我們不加static,能不能也讓它注入成功呢?

那無非就是在加載config類之前,把AutowiredAnnotationBeanPostProcessor提前加載到容器就可以了,那我們來看一下源碼是怎么加載這個實例的:

我們同樣可以依葫蘆畫瓢,看看在哪里提前加載比較合適,發現postProcessBeanFactory()這個方法比較合適,於是改成:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
            @SneakyThrows
            @Override
            public void initPropertySources() {
                YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
                List<PropertySource<?>> propertySources = loader.load("my-properties",
                        new FileSystemResource("/Users/xxx/spring-boot-study/src/main/resources/application.yml"));
                getEnvironment().getPropertySources().addLast(propertySources.get(0));
            }

            @Override
            protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
                String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
                beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
            }
        };
        Config config = context.getBean(Config.class);
        System.out.println(config);
    }
}

輸出:
Config(myValue=${my.value})

從結果來看,還是沒注入成功啊,經過一番調試,發現是在下面步驟中出了問題:

我們來看一下加載成功的情況:

embeddedValueResolver是在下面步驟中被添加進去的:

可以看出是在refresh()中的finishBeanFactoryInitialization()這個方法里面添加進去的,所以我們也要提前搞一下:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
            @SneakyThrows
            @Override
            public void initPropertySources() {
                YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
                List<PropertySource<?>> propertySources = loader.load("my-properties",
                        new FileSystemResource("/Users/xxx/spring-boot-study/src/main/resources/application.yml"));
                getEnvironment().getPropertySources().addLast(propertySources.get(0));
            }

            @Override
            protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
                String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
                beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
                beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
            }
        };
        Config config = context.getBean(Config.class);
        System.out.println(config);
    }
}

輸出:
Config(myValue=hello)

好了,大功告成!

總結

看到這里,相信大家都知道@Value為什么加載不成功了吧,主要就是因為加載順序的關系,可以看出最簡單的方法就是在方法上加一個static,后面的探究主要是地對Spring容器加載順序的理解

本文探究的是在配置類里存在BeanFactoryPostProcessor,如果換成BeanPostProcessor呢?同樣會加載不成功嗎?又是因為什么原因呢?其實也可以用同樣的方法來測試,和本文講的如出一轍,小伙伴們可自行探究一下。

有什么問題歡迎一起探討~~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM