详解@ConfigurationProperties实现原理与实战

在SpringBoot中,当需要获取到配置文件数据时,除了可以用Spring自带的@Value注解外,SpringBoot提供了一种更加方便的方式:@ConfigurationProperties。只要在bean上添加上这个注解,指定好配置文件的前缀,那么对应的配置文件数据就会自动填充到bean中。举个栗子,现在有如下配置:

myconfig.name=test
myconfig.age=22
myconfig.desc=这是我的测试描述

添加对应的配置类,并添加上注解@ConfigurationProperties,指定前缀为myconfig

@Component
@ConfigurationProperties(prefix = "myconfig")
public class MyConfig {
private String name;
private Integer age;
private String desc;
  //get/set 略
  @Override
public String toString() {
	return "MyConfig [name=" + name + ", age=" + age + ", desc=" + desc + "]";
}
}

添加使用:

public static void main(String[] args) throws Exception {
	SpringApplication springApplication = new SpringApplication(Application.class);
	// 非web环境
	springApplication.setWebEnvironment(false);
	ConfigurableApplicationContext application = springApplication.run(args);

	MyConfig config = application.getBean(MyConfig.class);
	log.info(config.toString());
	application.close();
}

可以看到输出log

com.cml.chat.lesson.lesson3.Application - MyConfig [name=test, age=22, desc=这是我的测试描述]

对应的属性都注入了配置中的值,而且不需要其他操作。是不是非常神奇?那么下面来剖析下@ConfigurationProperties到底做了啥?

首先进入@ConfigurationProperties源码中,可以看到如下注释提示:

See Also 中给我们推荐了ConfigurationPropertiesBindingPostProcessor,EnableConfigurationProperties两个类,EnableConfigurationProperties先放到一边,因为后面的文章中会详解EnableXX框架的实现原理,这里就先略过。那么重点来看看ConfigurationPropertiesBindingPostProcessor,光看类名是不是很亲切?不知上篇文章中讲的BeanPostProcessor还有印象没,没有的话赶紧回头看看哦。

ConfigurationPropertiesBindingPostProcessor
一看就知道和BeanPostProcessor有扯不开的关系,进入源码可以看到,该类实现的BeanPostProcessor和其他多个接口:

public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
	BeanFactoryAware, EnvironmentAware, ApplicationContextAware, InitializingBean,
	DisposableBean, ApplicationListener<ContextRefreshedEvent>, PriorityOrdered 

这里是不是非常直观,光看类的继承关系就可以猜出大概这个类做了什么。
BeanFactoryAware,EnvironmentAware,ApplicationContextAware是Spring提供的获取Spring上下文中指定对象的方法而且优先于BeanPostProcessor调用,至于如何工作的后面的文章会进行详解,这里只要先知道下作用就可以了。
此类同样实现了InitializingBean接口,从上篇文章中已经知道了InitializingBean是在BeanPostProcessor.postProcessBeforeInitialization之后调用,那么postProcessBeforeInitialization目前就是我们需要关注的重要入口方法。

先上源码看看:

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
		throws BeansException {
	//直接通过查找添加了ConfigurationProperties注解的的类
	ConfigurationProperties annotation = AnnotationUtils
			.findAnnotation(bean.getClass(), ConfigurationProperties.class);
	if (annotation != null) {
		postProcessBeforeInitialization(bean, beanName, annotation);
	}
	//查找使用工厂bean中是否有ConfigurationProperties注解
	annotation = this.beans.findFactoryAnnotation(beanName,
			ConfigurationProperties.class);
	if (annotation != null) {
		postProcessBeforeInitialization(bean, beanName, annotation);
	}
	return bean;
}

private void postProcessBeforeInitialization(Object bean, String beanName,
		ConfigurationProperties annotation) {
	Object target = bean;
	PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
			target);
	factory.setPropertySources(this.propertySources);
	factory.setValidator(determineValidator(bean));
	// If no explicit conversion service is provided we add one so that (at least)
	// comma-separated arrays of convertibles can be bound automatically
	factory.setConversionService(this.conversionService == null
			? getDefaultConversionService() : this.conversionService);
	if (annotation != null) {
		factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
		factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
		factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
		factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
		if (StringUtils.hasLength(annotation.prefix())) {
			factory.setTargetName(annotation.prefix());
		}
	}
	try {
		factory.bindPropertiesToTarget();
	}
	catch (Exception ex) {
		String targetClass = ClassUtils.getShortName(target.getClass());
		throw new BeanCreationException(beanName, "Could not bind properties to "
				+ targetClass + " (" + getAnnotationDetails(annotation) + ")", ex);
	}
}

在postProcessBeforeInitialization方法中,会先去找所有添加了ConfigurationProperties注解的类对象,找到后调用postProcessBeforeInitialization进行属性数据装配。

那么现在可以将实现拆分成如何寻找和如何装配两部分来说明,首先先看下如何查找到ConfigurationProperties注解类。

查找ConfigurationProperties

在postProcessBeforeInitialization方法中先通过AnnotationUtils查找类是否添加了@ConfigurationProperties注解,然后再通过 this.beans.findFactoryAnnotation(beanName,
ConfigurationProperties.class);继续查找,下面详解这两步查找的作用。

AnnotationUtils

AnnotationUtils.findAnnotation(bean.getClass(),ConfigurationProperties.class);这个是Spring中常用的工具类了,通过反射的方式获取类上的注解,如果此类添加了注解@ConfigurationProperties那么这个方法会返回这个注解对象和类上配置的注解属性。

beans.findFactoryAnnotation

这里的beans是ConfigurationBeanFactoryMetaData对象。在Spring中,可以以工厂bean的方式添加bean,这个类的作用就是在工程bean中找到@ConfigurationProperties注解。下面分析下实现过程:

ConfigurationBeanFactoryMetaData

public class ConfigurationBeanFactoryMetaData implements BeanFactoryPostProcessor {

private ConfigurableListableBeanFactory beanFactory;

private Map<String, MetaData> beans = new HashMap<String, MetaData>();

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
		throws BeansException {
	this.beanFactory = beanFactory;
 //迭代所有的bean定义,找出那些是工厂bean的对象添加到beans中
	for (String name : beanFactory.getBeanDefinitionNames()) {
		BeanDefinition definition = beanFactory.getBeanDefinition(name);
		String method = definition.getFactoryMethodName();
		String bean = definition.getFactoryBeanName();
		if (method != null && bean != null) {
			this.beans.put(name, new MetaData(bean, method));
		}
	}
}

public <A extends Annotation> Map<String, Object> getBeansWithFactoryAnnotation(
		Class<A> type) {
	Map<String, Object> result = new HashMap<String, Object>();
	for (String name : this.beans.keySet()) {
		if (findFactoryAnnotation(name, type) != null) {
			result.put(name, this.beanFactory.getBean(name));
		}
	}
	return result;
}

public <A extends Annotation> A findFactoryAnnotation(String beanName,
		Class<A> type) {
	Method method = findFactoryMethod(beanName);
	return (method == null ? null : AnnotationUtils.findAnnotation(method, type));
}

//略...

private static class MetaData {
	private String bean;
	private String method;
  //构造方法和其他方法略...
}

}

通过以上代码可以得出ConfigurationBeanFactoryMetaData的工作机制,通过实现BeanFactoryPostProcessor,在回调方法postProcessBeanFactory中,查找出所有通过工厂bean实现的对象,并将其保存到beans map中,通过方法findFactoryAnnotation可以查询到工厂bean中是否添加了对应的注解。那么这里的功能就是查找工厂bean中有添加@ConfigurationProperties注解的类了。

属性值注入

通过上述步骤,已经确认了当前传入的bean是否添加了@ConfigurationProperties注解。如果添加了则下一步就需要进行属性值注入了,核心代码在方法postProcessBeforeInitialization中:

private void postProcessBeforeInitialization(Object bean, String beanName,
		ConfigurationProperties annotation) {
	Object target = bean;
	PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
			target);
	//重点,这里设置数据来源
	factory.setPropertySources(this.propertySources);
	factory.setValidator(determineValidator(bean));
	//设置转换器
	factory.setConversionService(this.conversionService == null
			? getDefaultConversionService() : this.conversionService);
	if (annotation != null) {
	//将annotation中配置的属性配置到factory中
	}
	try {
	  //这里是核心,绑定属性值到对象中
		factory.bindPropertiesToTarget();
	}
	catch (Exception ex) {
	//抛出异常
	}
}

继续跟进factory.bindPropertiesToTarget方法,在bindPropertiesToTarget方法中,调用的是doBindPropertiesToTarget方法:

private void doBindPropertiesToTarget() throws BindException {
	RelaxedDataBinder dataBinder
  //略...
  //1、获取bean中所有的属性名称
  Set<String> names = getNames(relaxedTargetNames);
  //2、将属性名称和前缀转换为配置文件的key值
  PropertyValues propertyValues = getPropertySourcesPropertyValues(names,relaxedTargetNames);
  //3、通过上面两个步骤找到的属性从配置文件中获取数据通过反射注入到bean中
	dataBinder.bind(propertyValues);
	//数据校验
	if (this.validator != null) {
		dataBinder.validate();
	}
	//判断数据绑定过程中是否有错误
	checkForBindingErrors(dataBinder);
}

上面代码中使用dataBinder.bind方法进行属性值赋值,源码如下:

public void bind(PropertyValues pvs) {
	MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ?
			(MutablePropertyValues) pvs : new MutablePropertyValues(pvs);
	doBind(mpvs);
}
protected void doBind(MutablePropertyValues mpvs) {
	checkAllowedFields(mpvs);
	checkRequiredFields(mpvs);
	//进行赋值
	applyPropertyValues(mpvs);
}
protected void applyPropertyValues(MutablePropertyValues mpvs) {
	try {
		// Bind request parameters onto target object.
		getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
	}
	catch (PropertyBatchUpdateException ex) {
		// Use bind error processor to create FieldErrors.
		for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
			getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
		}
	}
}

经过以上步骤连续的方法调用后,最终调用的是ConfigurablePropertyAccessor.setPropertyValues使用反射进行设置属性值,到这里就不继续深入了。想要继续深入了解的可以继续阅读源码,到最后可以发现调用的是AbstractNestablePropertyAccessor.processLocalProperty中使用反射进行赋值。

上面的代码分析非常清晰明了的解释了如何查找@ConfigurationProperties对象和如何使用反射的方式进行赋值。

总结

在上面的步骤中我们分析了@ConfigurationProperties从筛选bean到注入属性值的过程,整个过程的难度还不算高,没有什么特别的难点,这又是一个非常好的BeanPostProcessor使用场景说明。
从本文中可以学习到BeanPostProcessor是在SpringBoot中运用,以及如何通过AnnotationUtils与ConfigurationBeanFactoryMetaData结合对系统中所有添加了指定注解的bean进行扫描。

到此这篇关于详解@ConfigurationProperties实现原理与实战的文章就介绍到这了,更多相关@ConfigurationProperties原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • @ConfigurationProperties绑定配置信息至Array、List、Map、Bean的实现

    相关说明: 在SpringBoot中,我们可以通过以下几种方式获取并绑定配置文件中的信息: @Value注解. 使用Environment. @ConfigurationProperties注解. 通过实现ApplicationListener接口,注册监听器,进行硬编码获取,可参考:https://www.jb51.net/article/187407.htm 硬编码加载文件获取. -- 注:一般情况下,第一种.第二种就够用了;但是如果想直接从配置文件中获取到数组.list.map.对象的话,

  • SpringBoot @ConfigurationProperties使用详解

    简介 本文将会详细讲解@ConfigurationProperties在Spring Boot中的使用. 添加依赖关系 首先我们需要添加Spring Boot依赖: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <relativePath/> <!-- lookup

  • Spring Boot2.0 @ConfigurationProperties使用详解

    引言 Spring Boot的一个便捷功能是外部化配置,可以轻松访问属性文件中定义的属性.本文将详细介绍@ConfigurationProperties的使用. 配置项目POM 在pom.xml中定义Spring-Boot 为parent <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId>

  • 详解@ConfigurationProperties实现原理与实战

    在SpringBoot中,当需要获取到配置文件数据时,除了可以用Spring自带的@Value注解外,SpringBoot提供了一种更加方便的方式:@ConfigurationProperties.只要在bean上添加上这个注解,指定好配置文件的前缀,那么对应的配置文件数据就会自动填充到bean中.举个栗子,现在有如下配置: myconfig.name=test myconfig.age=22 myconfig.desc=这是我的测试描述 添加对应的配置类,并添加上注解@Configuratio

  • 详解编译器编译原理

    详解编译器编译原理 什么是gcc  什么是gcc:gcc是GNU Compiler Collection的缩写.最初是作为C语言的编译器(GNU C Compiler),现在已经支持多种语言了,如C.C++.Java.Pascal.Ada.COBOL语言等. gcc支持多种硬件平台,甚至对Don Knuth 设计的 MMIX 这类不常见的计算机都提供了完善的支持 gcc主要特征  1)gcc是一个可移植的编译器,支持多种硬件平台 2)gcc不仅仅是个本地编译器,它还能跨平台交叉编译. 3)gcc

  • 详解Nginx 工作原理

    Nginx工作原理 Nginx由内核和模块组成. Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者.通常一个location中的指令会涉及一个handler模块和多个filter模块(当然,多个location可以复用同一个模块).handler模块负责处理请求,完成响应内容的生成,而filter模块对

  • 详解Python Celery和RabbitMQ实战教程

    前言 Celery是一个异步任务队列.它可以用于需要异步运行的任何内容.RabbitMQ是Celery广泛使用的消息代理.在本这篇文章中,我将使用RabbitMQ来介绍Celery的基本概念,然后为一个小型演示项目设置Celery .最后,设置一个Celery Web控制台来监视我的任务 基本概念   来!看图说话: Broker Broker(RabbitMQ)负责创建任务队列,根据一些路由规则将任务分派到任务队列,然后将任务从任务队列交付给worker Consumer (Celery Wo

  • 详解Unique SQL原理和应用

    1.什么是Unique SQL 用户执行SQL语句时,每一个SQL语句文本都会进入解析器(Parser),生成"解析树"(parse tree).遍历解析树中各个结点,忽略其中的常数值,以一定的算法结合树中的各结点,计算出来一个整数值,用来唯一标识这一类SQL,这个整数值被称为Unique SQL ID,Unique SQL ID相同的SQL语句属于同一个"Unique SQL". 例如,用户先后输入如下两条SQL语句: select * from t1 where

  • 详解MyBatis工作原理

    一.Mybatis工作原理 Mybatis分层框架图 Mybatis工作原理图 源码分析:一般都是从helloworld入手 1.根据xml配置文件(全局配置文件mybatis-config.xml)创建一个SqlsessionFactory对象,mybatis-config.xml有数据源一些环境信息 2.sql映射文件EmployeeMapper.xml配置了每一个sql,以及sql的封装规则等. 3.将sql映射文件注册在全局配置文件中 4.写代码: 根据全局配置文件得到sqlsessio

  • 详解Vue数据驱动原理

    前言 Vue区别于传统的JS库,例如JQuery,其中一个最大的特点就是不用手动去操作DOM,只需要对数据进行变更之后,视图也会随之更新. 比如你想修改div#app里的内容: /// JQuery <div id="app"></div> <script> $('#app').text('lxb') </script> <template> <div id="app">{{ message }

  • 详解Redis复制原理

    前言 本文主要介绍Redis复制机制 一.配置与实践 配置 Redis实例分为主节点(master)和从节点(slave),默认情况下都是主节点.每一个从节点只能有一个主节点,但是每一个主节点可以有多个从节点(注意数量,多个从节点会导致主节点写命令多次发送从而过度消耗网络带宽,可用树状结构降低主节点负载).复制是单向的,只能从主节点复制到从节点.配置复制的方式由以下3种: 在redis-slave.conf配置文件中加入slaveof {masterHost} {masterPort} 在red

  • 详解PID控制器原理

    一.P - Proportional 比例 想象一下一个全速行进的机器人,假设传感器上的值为1000. 现在,由于它的速度和惯性,它可能会超过一点, 当编写程序时,这可能是一个大麻烦,你想尽可能的准确.这个问题如图所示(x轴上的绿色标记代表理想距离): 在理想世界中,您告诉机器人在哪里停止,它就停止在哪里 但是,我们不是理想世界,如果我们突然告诉它停止,我们会有超调的问题,结果可能是这样的: 现在这个超调不会是一个问题,如果它的距离总是相同的.然而,有很多变量可以改变它超出的距离. 例如: 电池

  • 详解Python字符串原理与使用的深度总结

    目录 什么是 Python 字符串 ASCII 表与 Python 字符串字符 字符串属性 字符串方法 字符串操作 写在最后 今天我们来学习字符串数据类型相关知识,将讨论如何声明字符串数据类型,字符串数据类型与 ASCII 表的关系,字符串数据类型的属性,以及一些重要的字符串方法和操作,超级干货,不容错过! 什么是 Python 字符串 字符串是包含一系列字符的对象.字符是长度为 1 的字符串.在 Python 中,单个字符也是字符串.但是比较有意思的是,Python 编程语言中是没有字符数据类

随机推荐