Spring属性源解析及绑定核心原理【1】

版本说明

  • Spring Boot:【2.4.6】
  • Spring: 【5.3.7】
  • spring-cloud-netflix-eureka-client:【3.0.3】

@ConfigurationProperties注解用途

该注解可以用于批量绑定外部属性,可将属性绑定到一个POJO对象,而@Value一般只绑定一个值,一般和@Value可进行一起对比。官方文档有两者的比较

典型使用方式

用在 @Configuration配置类的@Bean修饰的方法上

@Configuration
public class ConnConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "shy.property")
    public ConnProperty readDruidDataSource() {
        return new ConnProperty();
    }
}

配置文件配置

shy:
  property:
    url: localhost:8090
    password: 08240819
    driver-class-name: test    # pojo的字段要转驼峰

用在类上

@Data
@ConfigurationProperties("shy.property")
public class ConnProperty {
    private String url;
    private String password;
}

@Configuration
@EnableConfigurationProperties(ConnProperty.class)
public class MyConfiguration {
    @Bean
    public ConnProperty connProperty() {
        return new ConnProperty();
    }
}

属性绑定的核心流程

  • 准备Environment,在prepareEnvironment阶段添加系统默认的属性源

    • ApplicationEnvironmentPreparedEvent事件发出后,由各种EnvironmentPostProcessor做后置处理,比如可以在这里添加自定义的属性源和属性值;这个过程还包括ConfigDataEnvironmentPostProcessor处理配置文件属性
  • 准备bd

  • 等bean实例化,populateBean填充属性之后,进入初始化阶段,将属性绑定到bean,执行完invokeAwareMethods后,调用applyBeanPostProcessorsBeforeInitialization;由ConfigurationPropertiesBindingPostProcessor处理bind,内部负责真正绑定处理的是Binder,关于Binder这里先不做详细讨论。

  • 如果需要还可以定义自己的BeanPostProcessor做postProcessAfterInitialization后置处理

  • 使用绑定好属性的bean

何时注入ConfigurationPropertiesBindingPostProcessor的bd信息

spring-boot-autoconfig的spring.factories定义了自动装配的配置类:ConfigurationPropertiesAutoConfiguration,定义了一个开关注解EnableConfigurationProperties

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
	String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";
	Class<?>[] value() default {};
}

使用@Import注解导入了一个EnableConfigurationPropertiesRegistrar,该类继承自ImportBeanDefinitionRegistrar,我们知道ImportBeanDefinitionRegistrar的子类可以往IoC容器中注入bd信息

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 注入bd信息
    // 注入ConfigurationPropertiesBindingPostProcessor的bd
    // 注入ConfigurationPropertiesBinder.Factory,ConfigurationPropertiesBinder的bd
		registerInfrastructureBeans(registry);
		registerMethodValidationExcludeFilter(registry);
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
		getTypes(metadata).forEach(beanRegistrar::register);
	}

属性源

先提几个问题,属性源有哪些,属性源的获取,属性源的优先级是怎样的(覆盖)。

关于外部化配置的优先级顺序,这篇官方文档给出了一些描述如下:

Spring Boot允许将配置外部化,这样就可以在不同的环境中使用相同的应用程序代码。可以使用属性文件、YAML文件、环境变量和命令行参数来外部化配置。属性值可以通过使用@Value直接注入到bean中,可以通过Spring的Environment抽象访问,也可以通过@ConfigurationProperties绑定到结构化对象。

Spring Boot使用一个非常特殊的PropertySource顺序,它被设计用来允许合理地重写值。属性按以下顺序考虑: 序号大的覆盖序号小

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as application.properties files).
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

添加属性源的核心流程对应到源码里,在SpringBoot启动的prepareEnvironment这个阶段,注意实际存储在MutablePropertySources中的PropertySource有一定顺序

	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    // 创建 StandardServletEnvironment,这是 WEB 环境默认的 Environment  
    // 创建该对象的过程中,会同时初始化 4 个 PropertySource,名称是:  
    // 1. servletConfigInitParams  
    // 2. servletContextInitParams  
    // 3. systemProperties  
    // 4. systemEnvironment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 1. 配置默认的配置源,DefaultPropertiesPropertySource  
    // 2. 解析命令行参数,作为一个 PropertySource: commandLineArgs
		configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 添加configurationProperties,attach
		ConfigurationPropertySources.attach(environment);
    // 发出 ApplicationEnvironmentPreparedEvent 事件,监听器Listener监听到事件后,会遍历EnvironmentPostProcessor注入一些配置源,包括  
    // 1. random  
    // 2. 替换systemEnvironment,加强上面的 systemEnvironment  
    // 3. spring.application.json  
    // 4. devtools  
    // 5. application.properties 等文件
    // .. 看有多少个EnvironmentPostProcessor做了处理
		listeners.environmentPrepared(bootstrapContext, environment);
		DefaultPropertiesPropertySource.moveToEnd(environment);
    // 解析 spring.xxx.xxx 配置
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
    // 再次attach
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

执行完这个方法查看下加入了哪些source,顺序示例如下

具体排序是如何实现的

springboot解析属性源绑定到bean上,就是按照一定的优先级顺序的。可以打断点查看具体的排序

**如何排序:**其实顺序就是添加到MutablePropertySources list的顺序,通过(addFirst,addLast)等;但是添加之前获取各种EnvironmentPostProcessor也有一定的优先级,对应EnvironmentPostProcessor的排序是通过Order,最终通过EnvironmentPostProcessor遍历顺序+addLast/addFirst来确定列表的顺序。

如ConfigDataEnvironmentPostProcessor的postProcessEnvironment做的处理

private void applyToEnvironment(ConfigDataEnvironmentContributors contributors,
			ConfigDataActivationContext activationContext, Set<ConfigDataLocation> loadedLocations,
			Set<ConfigDataLocation> optionalLocations) {
  	// ....
		MutablePropertySources propertySources = this.environment.getPropertySources();
		this.logger.trace("Applying config data environment contributions");
  	// 遍历contributors
		for (ConfigDataEnvironmentContributor contributor : contributors) {
			PropertySource<?> propertySource = contributor.getPropertySource();
			if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT && propertySource != null) {
				if (!contributor.isActive(activationContext)) {
					this.logger.trace(
							LogMessage.format("Skipping inactive property source '%s'", propertySource.getName()));
				}
				else {
					this.logger
							.trace(LogMessage.format("Adding imported property source '%s'", propertySource.getName()));
          // 最终就是添加到source list末尾
					propertySources.addLast(propertySource);
					this.environmentUpdateListener.onPropertySourceAdded(propertySource, contributor.getLocation(),
							contributor.getResource());
				}
			}
		}
  	// ....
	}

bind属性

bind发生在ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    // 检查有没有@ConfigurationProperties注解,有则绑定
		bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
		return bean;
	}

	private void bind(ConfigurationPropertiesBean bean) {
    // 如果没有ConfigurationProperties注解声明,bean为null,不执行绑定逻辑
		if (bean == null || hasBoundValueObject(bean.getName())) {
			return;
		}
		Assert.state(bean.getBindMethod() == BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '"
				+ bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean");
		try {
      // 绑定
			this.binder.bind(bean);
		}
		catch (Exception ex) {
			throw new ConfigurationPropertiesBindException(bean, ex);
		}
	}

在ConfigurationPropertiesBean#get方法里会检查是否有ConfigurationProperties注解,如果没有改注解,则不执行绑定逻辑

	// ConfigurationPropertiesBean#get
	public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
		Method factoryMethod = findFactoryMethod(applicationContext, beanName);
		return create(beanName, bean, bean.getClass(), factoryMethod);
	}
	// ConfigurationPropertiesBean#get
	private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
    // 找有没有ConfigurationProperties注解声明
		ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
		if (annotation == null) {
			return null;	//没有直接返回null
		}
		Validated validated = findAnnotation(instance, type, factory, Validated.class);
		Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
				: new Annotation[] { annotation };
		ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
				: ResolvableType.forClass(type);
		Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
		if (instance != null) {
			bindTarget = bindTarget.withExistingValue(instance);
		}
		return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
	}

真正的绑定是在Binder相关类中做的,bind前要去属性源列表里查找相应的属性

查找属性

	private ConfigurationProperty findProperty(ConfigurationPropertyName name, Context context) {
		if (name.isEmpty()) {
			return null;
		}
    // 这个context.getSources()是个特殊的迭代器,里面做了一些过滤处理,但是顺序不变
		for (ConfigurationPropertySource source : context.getSources()) {
			ConfigurationProperty property = source.getConfigurationProperty(name);
			if (property != null) {
				return property;
			}
		}
		return null;
	}

context.getSources()会返回迭代器,并将不需要的source进行过滤

/**
 * Adapter to convert Spring's {@link MutablePropertySources} to
 * {@link ConfigurationPropertySource ConfigurationPropertySources}.
 *
 * @author Phillip Webb
 */
class SpringConfigurationPropertySources implements Iterable<ConfigurationPropertySource> {

	private final Iterable<PropertySource<?>> sources;

	private final Map<PropertySource<?>, ConfigurationPropertySource> cache = new ConcurrentReferenceHashMap<>(16,
			ReferenceType.SOFT);

	SpringConfigurationPropertySources(Iterable<PropertySource<?>> sources) {
		Assert.notNull(sources, "Sources must not be null");
		this.sources = sources;
	}

	@Override
	public Iterator<ConfigurationPropertySource> iterator() {
		return new SourcesIterator(this.sources.iterator(), this::adapt);
	}

	private ConfigurationPropertySource adapt(PropertySource<?> source) {
		ConfigurationPropertySource result = this.cache.get(source);
		// Most PropertySources test equality only using the source name, so we need to
		// check the actual source hasn't also changed.
		if (result != null && result.getUnderlyingSource() == source) {
			return result;
		}
		result = SpringConfigurationPropertySource.from(source);
		this.cache.put(source, result);
		return result;
	}

	private static class SourcesIterator implements Iterator<ConfigurationPropertySource> {

		private final Deque<Iterator<PropertySource<?>>> iterators;

		private ConfigurationPropertySource next;

		private final Function<PropertySource<?>, ConfigurationPropertySource> adapter;

		SourcesIterator(Iterator<PropertySource<?>> iterator,
				Function<PropertySource<?>, ConfigurationPropertySource> adapter) {
			this.iterators = new ArrayDeque<>(4);
			this.iterators.push(iterator);
			this.adapter = adapter;
		}

		@Override
		public boolean hasNext() {
			return fetchNext() != null;
		}

		@Override
		public ConfigurationPropertySource next() {
			ConfigurationPropertySource next = fetchNext();
			if (next == null) {
				throw new NoSuchElementException();
			}
			this.next = null;
			return next;
		}

		private ConfigurationPropertySource fetchNext() {
			if (this.next == null) {
				if (this.iterators.isEmpty()) {
					return null;
				}
				if (!this.iterators.peek().hasNext()) {
					this.iterators.pop();
					return fetchNext();
				}
				PropertySource<?> candidate = this.iterators.peek().next();
				if (candidate.getSource() instanceof ConfigurableEnvironment) {
					push((ConfigurableEnvironment) candidate.getSource());
					return fetchNext();
				}
        // 判断是否要忽略这个source
				if (isIgnored(candidate)) {
					return fetchNext();
				}
				this.next = this.adapter.apply(candidate);
			}
			return this.next;
		}

		private void push(ConfigurableEnvironment environment) {
			this.iterators.push(environment.getPropertySources().iterator());
		}

		private boolean isIgnored(PropertySource<?> candidate) {
      // 忽略Random source,忽略StubPropertySource,忽略ConfigurationPropertySourcesPropertySource类型
			return (isRandomPropertySource(candidate) || candidate instanceof StubPropertySource
					|| candidate instanceof ConfigurationPropertySourcesPropertySource);
		}

		private boolean isRandomPropertySource(PropertySource<?> candidate) {
			Object source = candidate.getSource();
			return (source instanceof Random) || (source instanceof PropertySource<?>
					&& ((PropertySource<?>) source).getSource() instanceof Random);
		}

	}

}

举例

这里以Eureka Client的配置案例举例,默认下Eureka Client进行服务注册和服务发现,如下图

现在想实现的效果是默认不让Eureka Client进行服务注册和服务发现

声明一个DisableEurekaDiscoveryEnvironmenPostProcessor,并在spring.factories进行声明,保证SpringBoot的自动装配能起作用,进而能把DisableEurekaDiscoveryEnvironmenPostProcessor注入到容器中,但要让其优先级只要比ConfigDataEnvironmentPostProcessor高即可

SpringBoot程序启动后,创建好Environment后,发布environmentPrepared事件时,EnvironmentPostProcessorApplicationListener监听到这个事件

这样保证environmentPrepared事件发出后,EnvironmentPostProcessor的执行顺序是DisableDiscorveryEnvironmentPostProcessor优先于ConfigDataEnvironmentPostProcessor,因此两种EnvironmentPostProcessor的addLast操作之后就保证了属性源列表的顺序

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		ConfigurableEnvironment environment = event.getEnvironment();
		SpringApplication application = event.getSpringApplication();
    // 调用各种EnvironmentPostProcessor的postProcessEnvironment
    // DisableEurekaDiscoveryEnvironmenPostProcessor 就是一个EnvironmentPostProcessor并且优先级比ConfigDataEnvironmentPostProcessor高
		for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(event.getBootstrapContext())) {
			postProcessor.postProcessEnvironment(environment, application);
		}
	}

所以在refresh核心流程,实例化剩余的Bean(non-lazy-init)之前,context持有的environment属性里的propertySouce有了一个排序好的顺序

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

有顺序的propertySource,这里看到自定义的属性源discoveryProperties优先于配置文件的优先级

最终在绑定bean的时候,从属性源获取属性也是基于已经排序好的顺序遍历,因此优先获取到discoveryProperties中属性进行绑定,实现了覆盖EurekaClient默认配置的效果。

核心类总结

  • PropertySource核心抽象类:属性源的抽象,MapPropertySource,CommandLinePropertySource;PropertySources接口
  • PropertyResolver接口,子接口Environment:表示当前程序运行的环境profile。抽象了程序环境的关键要素:profile和properties。Environment = Profile + Properties;Environment的实现有很多
  • EnvironmentPostProcessor,ConfigDataEnvironmentPostProcessor
  • SpringConfigurationPropertySources:适配器,MutablePropertySources to ConfigurationPropertySources,里面维护了一个定制的迭代器
  • ConfigurationPropertiesBindingPostProcessor,ConfigurationPropertiesBinder,Binder
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值