Spring Boot中@ConfigurationProperties注解实现原理源码解析

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
全局流量管理 GTM,标准版 1个月
简介: 本文简述@ConfigurationProperties注解实现原理源码及源码解析

开源项目推荐

Pepper Metrics是我与同事开发的一个开源工具(https://github.com/zrbcool/pepper-metrics),其通过收集jedis/mybatis/httpservlet/dubbo/motan的运行性能统计,并暴露成prometheus等主流时序数据库兼容数据,通过grafana展示趋势。其插件化的架构也非常方便使用者扩展并集成其他开源组件。
请大家给个star,同时欢迎大家成为开发者提交PR一起完善项目。

  1. 概述

不用说大家都知道Spring Boot非常的方便,快捷,让开发的同学简单的几行代码加上几行配置甚至零配置就能启动并使用一个项目,项目当中我们也可能经常使用 @ConfigurationProperties将某个Bean与properties配置当中的prefix相绑定,使配置值与定义配置的Bean分离,方便管理。
那么,这个@ConfigurationProperties是什么机制,如何实现的呢?我们今天来聊聊这个话题

  1. 正文

2.1 从EnableConfigurationProperties说起

为什么从EnableConfigurationProperties讲? Spring Boot项目自身当中大量autoconfigure都是使用EnableConfigurationProperties注解启用XXXProperties功能,例如spring-data-redis的 这个RedisAutoConfiguration

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class) //看这里
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

// ...

}
而RedisProperties中又带有注解@ConfigurationProperties(prefix = "spring.redis"),这样就将spring.redis这个前缀的配置项与RedisProperties 这个实体类进行了绑定。

2.2 EnableConfigurationProperties内部实现解析

说完来由,我们就来说说内部实现,先来看看

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {

Class<?>[] value() default {};

}
@Import(EnableConfigurationPropertiesImportSelector.class)指明了这个注解的处理类EnableConfigurationPropertiesImportSelector, 查看EnableConfigurationPropertiesImportSelector源码

class EnableConfigurationPropertiesImportSelector implements ImportSelector {

private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(),
        ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

@Override
public String[] selectImports(AnnotationMetadata metadata) {
    return IMPORTS;
}

// 省略部分其他方法

}
我们先看这块关键部分返回了一个IMPORTS数组,数组中包含ConfigurationPropertiesBeanRegistrar.class,ConfigurationPropertiesBindingPostProcessorRegistrar.class两个元素
根据@Import及ImportSelector接口的原理(其原理可以参考同事写的一篇文章:相亲相爱的@Import和@EnableXXX),我们得知spring会初始化上面两个Registrar到spring容器当中,而两个Registrar均实现了ImportBeanDefinitionRegistrar接口, 而ImportBeanDefinitionRegistrar会在处理Configuration时触发调用(其原理可以参考文章:这块找一篇文章),下面我们分别深入两个Registrar的源码:

ConfigurationPropertiesBeanRegistrar
ConfigurationPropertiesBindingPostProcessorRegistrar
2.2.1 ConfigurationPropertiesBindingPostProcessorRegistrar

直接看代码

public class ConfigurationPropertiesBindingPostProcessorRegistrar implements ImportBeanDefinitionRegistrar {

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    if (!registry.containsBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
        registerConfigurationPropertiesBindingPostProcessor(registry);
        registerConfigurationBeanFactoryMetadata(registry);
    }
}

private void registerConfigurationPropertiesBindingPostProcessor(BeanDefinitionRegistry registry) {
    GenericBeanDefinition definition = new GenericBeanDefinition();
    definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
    definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    registry.registerBeanDefinition(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);
}

private void registerConfigurationBeanFactoryMetadata(BeanDefinitionRegistry registry) {
    GenericBeanDefinition definition = new GenericBeanDefinition();
    definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
    definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME, definition);
}

}
可以看到注册了两个Bean到spring容器

ConfigurationPropertiesBindingPostProcessor
其实现如下接口: BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean
PriorityOrdered
Ordered.HIGHEST_PRECEDENCE + 1保证前期执行,且非最先
ApplicationContextAware
获取到ApplicationContext设置到内部变量
InitializingBean
afterPropertiesSet方法在Bean创建时被调用,保证内部变量configurationPropertiesBinder被初始化,这个binder类就是使prefix与propertyBean进行值绑定的关键工具类
BeanPostProcessor postProcessBeforeInitialization方法处理具体的bind逻辑如下:

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
      ConfigurationProperties annotation = getAnnotation(bean, beanName, ConfigurationProperties.class);
      if (annotation != null) {
          bind(bean, beanName, annotation);
      }
      return bean;
  }
  private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
      ResolvableType type = getBeanType(bean, beanName);
      Validated validated = getAnnotation(bean, beanName, Validated.class);
      Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
              : new Annotation[] { annotation };
      Bindable<?> target = Bindable.of(type).withExistingValue(bean).withAnnotations(annotations);
      try {
          // 在这里完成了,关键的prefix到PropertyBean的值绑定部分,所以各种@ConfigurationProperties注解最终生效就靠这部分代码了
          this.configurationPropertiesBinder.bind(target);
      }
      catch (Exception ex) {
          throw new ConfigurationPropertiesBindException(beanName, bean, annotation, ex);
      }
  }

ConfigurationBeanFactoryMetadata
如果某些Bean是通过FactoryBean创建,则该类用于保存FactoryBean的各种原信息,用于ConfigurationPropertiesBindingPostProcessor当中的元数据查询,这里就不做展开
2.2.2 ConfigurationPropertiesBeanRegistrar

其实ConfigurationPropertiesBeanRegistrar是EnableConfigurationPropertiesImportSelector的静态内部类,在前面贴代码时被省略的部分,上代码

public static class ConfigurationPropertiesBeanRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        getTypes(metadata).forEach((type) -> register(registry, (ConfigurableListableBeanFactory) registry, type));
    }

    private List<Class<?>> getTypes(AnnotationMetadata metadata) {
        MultiValueMap<String, Object> attributes = metadata
                .getAllAnnotationAttributes(EnableConfigurationProperties.class.getName(), false);
        return collectClasses((attributes != null) ? attributes.get("value") : Collections.emptyList());
    }

    private List<Class<?>> collectClasses(List<?> values) {
        return values.stream().flatMap((value) -> Arrays.stream((Object[]) value)).map((o) -> (Class<?>) o)
                .filter((type) -> void.class != type).collect(Collectors.toList());
    }

    private void register(BeanDefinitionRegistry registry, ConfigurableListableBeanFactory beanFactory,
            Class<?> type) {
        String name = getName(type);
        if (!containsBeanDefinition(beanFactory, name)) {
            registerBeanDefinition(registry, name, type);
        }
    }

    private String getName(Class<?> type) {
        ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type, ConfigurationProperties.class);
        String prefix = (annotation != null) ? annotation.prefix() : "";
        return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
    }

    private void registerBeanDefinition(BeanDefinitionRegistry registry, String name, Class<?> type) {
        assertHasAnnotation(type);
        GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(type);
        registry.registerBeanDefinition(name, definition);
    }

    private void assertHasAnnotation(Class<?> type) {...}
    private boolean containsBeanDefinition(ConfigurableListableBeanFactory beanFactory, String name) {...}
}

逻辑解读:
主逻辑入口(registerBeanDefinitions)
1 -> getTypes(metadata)拿到标注EnableConfigurationProperties注解的配置值,整理成List然后逐个处理 2 -> 对每个元素(Class)调用register方法处理
3 -> register方法通过类当中的ConfigurationProperties注解的prefix值加类名字作为beanName通过registry.registerBeanDefinition调用将Class<?>注册到registry当中
当标注注解@ConfigurationProperties的XXXProperties的BeanDefinition加入到registry中后,Bean的初始化就交给spring容器, 而这个过程中前面提到的ConfigurationPropertiesBindingPostProcessorRegistrar就完成一系列的后置操作帮助我们完成最终的值绑定

  1. 总结

@ConfigurationProperties的整体处理过程,本文已经基本讲述完毕,现在大体总结一下: EnableConfigurationProperties完成ConfigurationPropertiesBindingPostProcessorRegistrar及ConfigurationPropertiesBeanRegistrar的引入 其中:

ConfigurationPropertiesBeanRegistrar完成标注@ConfigurationProperties的类的查找并组装成BeanDefinition加入registry
ConfigurationPropertiesBindingPostProcessorRegistrar完成ConfigurationPropertiesBindingPostProcessor及ConfigurationBeanFactoryMetadata
ConfigurationPropertiesBindingPostProcessor完成所有标注@ConfigurationProperties的Bean到prefix的properties值绑定
ConfigurationBeanFactoryMetadata仅用于提供上面处理中需要的一些元数据信息

  1. 作者其他文章

https://github.com/zrbcool/blog-public

目录
相关文章
|
14天前
|
XML Java 测试技术
Spring IOC—基于注解配置和管理Bean 万字详解(通俗易懂)
Spring 第三节 IOC——基于注解配置和管理Bean 万字详解!
95 26
|
17天前
|
缓存 Java 数据库
SpringBoot缓存注解使用
Spring Boot 提供了一套方便的缓存注解,用于简化缓存管理。通过 `@Cacheable`、`@CachePut`、`@CacheEvict` 和 `@Caching` 等注解,开发者可以轻松地实现方法级别的缓存操作,从而提升应用的性能和响应速度。合理使用这些注解可以大大减少数据库的访问频率,优化系统性能。
162 89
|
4天前
|
监控 Java Spring
SpringBoot:SpringBoot通过注解监测Controller接口
本文详细介绍了如何通过Spring Boot注解监测Controller接口,包括自定义注解、AOP切面的创建和使用以及具体的示例代码。通过这种方式,可以方便地在Controller方法执行前后添加日志记录、性能监控和异常处理逻辑,而无需修改方法本身的代码。这种方法不仅提高了代码的可维护性,还增强了系统的监控能力。希望本文能帮助您更好地理解和应用Spring Boot中的注解监测技术。
33 16
|
27天前
|
XML Java 应用服务中间件
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
200 17
Spring Boot 两种部署到服务器的方式
|
2月前
|
Java Spring 容器
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
60 21
|
9月前
|
安全 Java 应用服务中间件
阿里技术官架构使用总结:Spring+MyBatis源码+Tomcat架构解析等
分享Java技术文以及学习经验也有一段时间了,实际上作为程序员,我们都清楚学习的重要性,毕竟时代在发展,互联网之下,稍有一些落后可能就会被淘汰掉,因此我们需要不断去审视自己,通过学习来让自己得到相应的提升。
|
9月前
|
Java 关系型数据库 数据库连接
Spring源码解析--深入Spring事务原理
本文将带领大家领略Spring事务的风采,Spring事务是我们在日常开发中经常会遇到的,也是各种大小面试中的高频题,希望通过本文,能让大家对Spring事务有个深入的了解,无论开发还是面试,都不会让Spring事务成为拦路虎。
120 1
|
4月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
236 5
|
4月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
4月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
174 9

推荐镜像

更多