- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
本文基于Spring的5.3.8版本,探究了@Value注入不成功的原因,以及解决办法。其中包含了environment的理解,以及Spring容器的加载顺序的理解。
项目里想用@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})
上面的代码做了几件事情:
resources/application.yml
文件中定义了my.value=hello
Config
类,利用@value
注解将hello
注入到字段myValue
中Main
类测试效果测试类做了几件事情:
AnnotationConfigApplicationContext
这个容器加载配置类Config
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);
}
}
从结果来看:
environment
并没有包含我们application.yml
文件里的内容systemProperties
和systemEnvironment
那我们就需要把application.yml
文件里的内容加载到environment
,需要考虑以下两个问题:
yml
文件的内容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
呢?同样会加载不成功吗?又是因为什么原因呢?其实也可以用同样的方法来测试,和本文讲的如出一辙,小伙伴们可自行探究一下。
有什么问题欢迎一起探讨~~~
我正在尝试使用 flot 绘制 SQL 数据库中的数据图表,这是使用 php 收集的,然后使用 json 编码的。 目前看起来像: [{"month":"February","data":482},
我有一个来自 php 行的 json 结果,类似于 ["value"]["value"] 我尝试使用内爆函数,但得到的结果是“value”“value” |id_kategori|created_at
脚本 1 将记录 two 但浏览器仍会将 select 元素呈现为 One。该表单还将提交值 one。 脚本 2 将记录、呈现和提交 两个。我希望它们是同义词并做同样的事情。请解释它们为何不同,以及我
我的python字典结构是这样的: ips[host][ip] 每行 ips[host][ip] 看起来像这样: [host, ip, network, mask, broadcast, mac, g
在 C# 中 我正在关注的一本书对设置和获取属性提出了这样的建议: double pri_test; public double Test { get { return pri_test; }
您可能熟悉 enum 位掩码方案,例如: enum Flags { FLAG1 = 0x1, FLAG2 = 0x2, FLAG3 = 0x4, FLAG4 = 0x8
在一些地方我看到了(String)value。在一些地方value.toString() 这两者有什么区别,在什么情况下我需要使用哪一个。 new Long(value) 和 (Long)value
有没有什么时候 var result = !value ? null : value[0]; 不会等同于 var result = value ? value[0] : null; 最佳答案 在此处将
我正在使用扫描仪检测设备。目前,我的条形码的值为 2345345 A1。因此,当我扫描到记事本或文本编辑器时,输出将类似于 2345345 A1,这是正确的条形码值。 问题是: 当我第一次将条形码扫描
我正在读取 C# 中的资源文件并将其转换为 JSON 字符串格式。现在我想将该 JSON 字符串的值转换为键。 例子, [ { "key": "CreateAccount", "text":
我有以下问题: 我有一个数据框,最多可能有 600 万行左右。此数据框中的一列包含某些 ID。 ID NaN NaN D1 D1 D1 NaN D1 D1 NaN NaN NaN NaN D2 NaN
import java.util.*; import java.lang.*; class Main { public static void main (String[] args) thr
我目前正在开发我的应用程序,使其设计基于 Holo 主题。在全局范围内我想做的是工作,但我对文件夹 values、values-v11 和 values-v14. 所以我知道: values 的目标是
我遇到了一个非常奇怪的问题。 我的公司为我们的各种 Assets 使用集中式用户注册网络服务。我们一般通过HttpURLConnection使用请求方法GET向Web服务发送请求,通过qs设置参数。这
查询: UPDATE nominees SET votes = ( SELECT votes FROM nominees WHERE ID =1 ) +1 错误: You can't specify
如果我运行一段代码: obj = {}; obj['number'] = 1; obj['expressionS'] = 'Sin(0.5 * c1)'; obj['c
我正在为我的应用创建一个带有 Twitter 帐户的登录页面。当我构建我的项目时会发生上述错误。 values/strings.xml @dimen/abc_text_size_medium
我在搜索引擎中使用以下 View : CREATE VIEW msr_joined_view AS SELECT table1.id AS msr_id, table1.msr_number, tab
为什么验证会返回此错误。如何解决? ul#navigation li#navigation-3 a.current Value Error : background-position Too
我有一个数据名如下 import pandas as pd d = { 'Name' : ['James', 'John', 'Peter', 'Thomas', 'Jacob', 'Andr
我是一名优秀的程序员,十分优秀!