背景

项目里想用@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/tiger/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/tiger/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/tiger/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/tiger/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/tiger/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呢?同样会加载不成功吗?又是因为什么原因呢?其实也可以用同样的方法来测试,和本文讲的如出一辙,小伙伴们可自行探究一下。

有什么问题欢迎一起探讨~~~

【源码】“@Value 注入不成功”引发的一系列骚操作的更多相关文章

  1. Netty 源码解析(八): 回到 Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第八篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  2. Android源码阅读技巧--查找开发者选项中显示触摸操作源码

    在开发者模式下,在开发者选项中,可以勾选“显示触摸操作”,然后只要点击屏幕就会在点击的位置有圈圈显示.如何找到绘制圈圈的代码部分,有什么技巧来阅读代码量这么大的android系统源码呢?以下请跟着小老 ...

  3. [从源码学设计]蚂蚁金服SOFARegistry之延迟操作

    [从源码学设计]蚂蚁金服SOFARegistry之延迟操作 0x00 摘要 SOFARegistry 是蚂蚁金服开源的一个生产级.高时效.高可用的服务注册中心. 本系列文章重点在于分析设计和架构,即利 ...

  4. 读Hadoop3.2源码,深入了解java调用HDFS的常用操作和HDFS原理

    本文将通过一个演示工程来快速上手java调用HDFS的常见操作.接下来以创建文件为例,通过阅读HDFS的源码,一步步展开HDFS相关原理.理论知识的说明. 说明:本文档基于最新版本Hadoop3.2. ...

  5. 高级运维(六):源码安装Redis缓存服务、常用Redis数据库操作指令、配置Redis主从服务器

    一.源码安装Redis缓存服务 目标: 本案例要求先快速搭建好一台Redis服务器,并测试该缓存服务器: 1> 设置变量test,值为123 2> 查看变量test的值 3> 设置计 ...

  6. Lua5.4源码剖析:二. 详解String数据结构及操作算法

    概述 lua字符串通过操作算法和内存管理,有以下优点: 节省内存. 字符串比较效率高.(比较哈希值) 问题: 相同的字符串共享同一份内存么? 相同的长字符串一定不共享同一份内存么? lua字符串如何管 ...

  7. 学习 CLR 源码:连续内存块数据操作的性能优化

    目录 C# 原语类型 1,利用 Buffer 优化数组性能 2,BinaryPrimitives 细粒度操作字节数组 提高代码安全性 3,BitConverter.MemoryMarshal 4,Ma ...

  8. 16Aspx.com源码2013年10月到2013年12月详细

    创建时间FROM: 创建时间TO:   ExtJS合同管理信息系统源码 2013-12-13   [VS2008] 源码介绍: ExtJS合同管理信息系统源码浏览器兼容:IE,Firefox,谷歌等主 ...

  9. .Net Core缓存组件(Redis)源码解析

    上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储.在分析源 ...

随机推荐

  1. SQL Prompt快捷键

    1. DF DELETE FROM  2. ssf SELECT * FROM  3. be BEGIN  END 4. ij INNER JOIN  5. ap ALTER PROCEDURE  6 ...

  2. Blazor Server 和 WebAssembly 应用程序入门指南

    翻译自 Waqas Anwar 2021年3月12日的文章 <A Beginner's Guide To Blazor Server and WebAssembly Applications&g ...

  3. 教你几招HASH表查找的方法

    摘要:根据设定的哈希函数 H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的.地址连续的地址集 (区间) 上,并以关键字在地址集中的"象"作为相应记录在表中的存储 ...

  4. docker部署的nginx非80端口无法访问

    请检查nginx容器是否只开启了80端口映射!!! 请检查nginx容器是否只开启了80端口映射!!! 请检查nginx容器是否只开启了80端口映射!!! 如果你访问的端口在nignx容器已经开启了端 ...

  5. hive学习笔记之十:用户自定义聚合函数(UDAF)

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<hive学习笔记>的第十 ...

  6. LAMP——实现phpMyadmin、wordpress及Discuz应用部署

    一.环境准备 操作系统:Centos8.3.2011 软件:Apache2.4.37.Mysql8.0.21.PHP7.2.24 二.安装过程 1.安装phpmyadmin 1.1.安装软件包并启动服 ...

  7. RNA

    原始地球 你会想,我们每一个细胞中都有一个遗传分子叫做DNA?那么,DNA之前有没有什么遗传分子呢?我的答案是:"有".在远古地球,那个海底有无数火山,喷发的火山口两侧都是喷涌出的 ...

  8. MinIO关闭公开桶的列表展示(S3 browser)

    MinIO通过配置桶策略关闭列表展示,以下为操作教程. 下载:s3browser官网 安装完成S3 browser后,添加账号 修改桶policy,选择桶public右键 删除 s3:ListBuck ...

  9. 接口自动化框架搭建Unittes+HTMLTestRunner

    本次主要尝试搭建接口自动化框架,基于 unittest+HTMLTestRunner 框架主要模块: config: 存放配置文件 lib: 封装了一些接口前置函数:处理各种事物 log: 存放生成的 ...

  10. 深入理解 SynchronizationContext

    深入理解 SynchronizationContext 目录 深入理解 SynchronizationContext SynchronizationContext(后续以SC简称) 是什么? 1.1 ...