本来我是有在写一个自己的项目, 有一些配置类想将其配置变得优雅一些,就想着用配置文件的形式。也方便打包编译的时候快速切换不同环境。
由于是SpringBoot项目,那么用配置文件的形式呢,当然少不了@ConfigurationProperties 这个注解
使用了这个注解来将配置文件的值注入到配置类之中呢,我还不满足。 想着配置文件都放一个application.yml里或者bootstrap.yml里,也挺丑陋的。
就一个顺手将配置文件分离了,然后用 @PropertySource 来指定配置类所使用的配置文件具体路径。
然后我没有使用 @Component、@Configuration 之类的注解来将其注册到Spring上下文中。
使用的是 @EnableConfigurationProperties 这个注解, 为什么呢。这是因为使用此种方式可以在一个地方加载到所有的配置类,比较符合单一职责原则。以后配置多了要找的话比起每个类自己注册自己也要方便的多。
大体的话是一个这样子的形式:
@ConfigurationProperties(prefix = "myprefix")
@PropertySource(
value = "classpath:myconfig.yml",
factory = YamlPropertySourceFactory.class
)
@Data
public class MyProperties {
//...
}
@Configuration
@EnableConfigurationProperties({MyProperties.class})
public class ApplicationConfig {
}
根据我的经验来说是没有问题的。
可是偏偏它就出了问题了
开发环境:
IntelliJ IDEA: 2019.3
SpringBoot version: 2.2.4
项目启动后出现了奇怪的情景,启动没有报错,但是配置类中的属性注入失败了。
而启动没有报错,我又打断点看了一下使用此配置的地方。
就发现这个配置类可以被 Spring 成功的注入(即已作为一个 Bean 被 Spring 管理),但是里面的值却又都是默认值
我一时以为是我 yml 配置写错了,或者说我实现了 PropertySourceFactory 的加用来载 yml 配置文件的工厂类内部逻辑有问题。
之后就是各种测试,搞到后面心态都有些崩了
具体做了哪些实验就不说了,总之浪费了挺多无意义的时间。最终确定了几个情形:
1、 使用 @EnableConfigurationProperties 可以成功将 application.yml 中的配置加载进 Bean
2、若使用 @PropertySource 指定配置文件,则 @EnableConfigurationProperties 无法将指定的配置文件参数注入进 Bean
3、无论如何,在@EnableConfigurationProperties 设置的配置类都会被 Spring 实例化。
4、@PropertySource 指定配置文件后,若是在类上使用 @Configuration、@Component 注解形式来实现IOC,则 Spring 可以成功将配置文件的值注入进 Bean
出现了这种问题。就很令人疑惑,而我在网上找的资料都说 @EnableConfigurationProperties 可以正确加载配置文件。
而到了我这,这个Bean生成是生成了,但这个配置文件里的值怎么都注入不进去,就很怪。必须要用 @Component 这种注解形式来注册 Bean 才行。
既然遇到了这种问题,也没在网上找到具体的原因。那我就自己来分析分析,为什么会出现这种情况。
分析的话,那就只能看源码咯
我们首先来看一下@PropertySource这个东西是怎么被Spring解析出来的, 看下具体的源码,分析一下流程,先看看是不是在解析过程中出现的问题。
我们点开 PropertySource.class 文件, 在 IDEA 中按 ctrl+鼠标左键点击一下类名。 可以找到在什么地方引入了此class。
很轻松的可以定位到一个方法, 只有在这个方法之中, 才会被处理: org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass
这是他的判定逻辑( 为了方便观看,我去掉了其他的注解判定逻辑 ):
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {
//...
// 处理定义了 @PropertySources 注解的类
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
} else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
//...
return null;
}
继续,深入到 processPropertySource() 方法的源码中, 看看他是怎么处理的。
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
//资源名字提取
String name = propertySource.getString("name");
if (!StringUtils.hasLength(name)) {
name = null;
}
//编码方式
String encoding = propertySource.getString("encoding");
if (!StringUtils.hasLength(encoding)) {
encoding = null;
}
//获取所有的要加载的资源文件
String[] locations = propertySource.getStringArray("value");
Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
//是否忽略找不到的property source
boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
//取得设置的属性来源工厂。 默认的是 DefaultPropertySourceFactory。 只能加载 .properties 文件
Class extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
//遍历资源文件, 处理占位符后得到具体的资源(比如文件流)
for (String location : locations) {
try {
String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
Resource resource = this.resourceLoader.getResource(resolvedLocation);
//使用上面得到的工厂来处理资源生成属性源, 这一步的具体操作就是可以自己实现来定义的。 比如实现一个yml处理工厂
addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
} catch (IllegalArgumentException | FileNotFoundException | UnknownHostException ex) {
// Placeholders not resolvable or resource not found when trying to open it
if (ignoreResourceNotFound) {
if (logger.isInfoEnabled()) {
logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
}
} else {
throw ex;
}
}
}
}
这里我注释加的比较详细, 可以看到就是在这个方法内部根据@PropertySource 中定义的所有参数对我们具体的类做了处理,将配置文件的属性都注入进去。
一直往上翻动, 找到调用 ConfigurationClassParser#doProcessConfigurationClass 此方法最起始的入口点,最终我找到的是Spring的这个类 : ConfigurationClassPostProcessor
他的定义是这样子的:
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
}
他是一个 BeanDefinitionRegistryPostProcessor 的实现类。
而 BeanDefinitionRegistryPostProcessor 这个类熟悉 Spring Bean 生命周期的就知道,这玩意是用来增强 BeanDefinition 的。
是一个在 Spring 的 Bean 生命周期非常靠前的处理钩子,此时这个 BeanDefinition 还在解析中,都没注册到 BeanFactory 里去。
看名字其实就知道, ConfigurationClassPostProcessor 这个类是专门扫描、解析、注册所有配置类的。
而判断是否为配置类的方法我看了一下,里面写的是有@Configuration、@Component、@ComponentScan、@Import、@ImportResource、@Bean 这些注解定义的/加载的Class就是配置类。
结论:
可以知道, 声明为配置类从而初始化Bean实例,这样子不会有问题。
正常注册到Spring容器内部的Bean定义, 类上使用@PropertySource 注解可以成功的被 ConfigurationClassPostProcessor 这个类处理,最终交给ConfigurationClassParser#doProcessConfigurationClass() 来解析。
看了下 @PropertySource 的解析流程,没发现问题,那就只能再看下 @EnableConfigurationProperties 究竟干了些什么咯
首先要做的是先点开 @EnableConfigurationProperties 这个注解
他是这样定义的:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
/**
* The bean name of the configuration properties validator.
* @since 2.2.0
*/
String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
/**
* Convenient way to quickly register
* {@link ConfigurationProperties @ConfigurationProperties} annotated beans with
* Spring. Standard Spring Beans will also be scanned regardless of this value.
* @return {@code @ConfigurationProperties} annotated beans to register
*/
Class>[] value() default {};
}
利用的Spring @Import 机制,来将 ImportBeanDefinitionRegistrar 实现导入, 从而对@EnableConfigurationProperties 内包含的内容进行解析。
他这个具体实现的源码读起来非常简单:
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerInfrastructureBeans(registry);
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
getTypes(metadata).forEach(beanRegistrar::register);
}
private Set> getTypes(AnnotationMetadata metadata) {
return metadata.getAnnotations().stream(EnableConfigurationProperties.class)
.flatMap((annotation) -> Arrays.stream(annotation.getClassArray(MergedAnnotation.VALUE)))
.filter((type) -> void.class != type).collect(Collectors.toSet());
}
@SuppressWarnings("deprecation")
static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
ConfigurationPropertiesBindingPostProcessor.register(registry);
ConfigurationPropertiesBeanDefinitionValidator.register(registry);
ConfigurationBeanFactoryMetadata.register(registry);
}
}
流程就是这么简单几步:
1、 初始化好BeanDefinition注册器
2、取得 @EnableConfigurationProperties value属性表示的所有 Class 对象
3、调用注册器的 register(Class class) 方法,将这些 Class表示的对象生成 BeanDefinition 注册到 Spring 上下文里
而 Spring 的 @Import 机制这里就得简单说一下。
根据我上边说明的@PropertySource 处理流程就可以知道,ConfigurationClassPostProcessor 是一个对所有的配置类进行处理的类。
而这个 @Import 注解,自然也会被其所解析。
然后我打了个debug看了下, 发现他是 Spring 在解析 @SpringBootApplication 这个启动类注解的时候,通过 ConfigurationClassBeanDefinitionReader 类的 loadBeanDefinitions() 方法顺带解析出来的。
深入到此方法里边去几层就可以找到 loadBeanDefinitionsFromRegistrars()这个方法, Spring 就是使用这个方法专门处理 @Import 注解。
loadBeanDefinitionsFromRegistrars 方法逻辑是这样的:
如果该类有@Import,且Import进来的类实现了ImportBeanDefinitionRegistrar接口,则调用Import进来的类的registerBeanDefinitions方法。
而@EnableConfigurationProperties 导入的 EnableConfigurationPropertiesRegistrar 究竟做了什么,上面已经解释的很清楚了。
他是手动将配置类生成出来然后直接生成 BeanDefinition 再将其注册到 BeanFactory 中的。
我就说为什么。原因经过这么一顿分析以后总算是明白了。
使用注解来实现IOC,会经过完整的 Bean 生命周期,所以 ConfigurationClassPostProcessor 会成功的处理相应配置。
EnableConfigurationPropertiesRegistrar 是在处理@SpringBootApplication这个配置时加载出来的。ConfigurationClassPostProcessor 经过倒是也经过了,不过处理的是项目启动类。
EnableConfigurationPropertiesRegistrar 它内部实现加载 @ConfigurationProperties 修饰的类时,都不会走那个完整的Bean 生命周期,直接生成 BeanDefinition 就往 BeanFactory 里塞了。
所以也没有地方会对 @PropertySource 注解进行处理了。
那为啥网上的人说 @EnableConfigurationProperties 可以成功的导入自定义配置呢? 我看了下,@EnableConfigurationProperties 他在SpringBoot 2.2.0以前 @Import 导入的不是 EnableConfigurationPropertiesRegistrar 这个类
这个类是在SpringBoot 2.2.0以后新建并更新上去的。