posts - 156,  comments - 601,  trackbacks - 0

1. Springboot config示例与原理介绍

1.1 Springboot config示例

Springboot给我们提供了非常便捷与方便的配置使用方式,常用的使用示例有以下两种:

1)使用@ConfigurationProperties 读取多个属性

使用方法:@ConfigurationProperties(prefix = "spring.datasource")

使用说明:提供 Setter方法 和 标记组件 Component

@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class MyDataSource {
 
    private String url;
 
    private String username;
 
    private String password;
 
    private String driverClassName;
 
    // 提供Setter 和 Getter 方法
}



2)使用@Value 读取单个属性

使用方法:@Value("spring.datasource.XXX")

使用说明:提供 Setter方法 和 标记组件 Component

@Component
public class MyDataSource {
 
    @Value("spring.datasource.url")
    private String url;
 
    @Value("spring.datasource.username")
    private String username;
 
    @Value("spring.datasource.password")
    private String password;
 
    @Value("spring.datasource.driver-class-name")
    private String driverClassName;
 
    // 提供Setter 和 Getter 方法
}


接下来针对两个注解@Value 和 @ConfigurationProperties, 他们是如何工作的呢?

1.2 SpringBoot 配置原理

springboot的配置是伴随Springboot的启动进行的, 主要包括配置加载、存储、替换3个基本过程。

1) SpringBoot配置加载

       在SpringBoot的启动过程中,会创建一个ConfigurableEnvironment来存放不同的配置,不同来源的配置被加载到不同的PropertySource并添加到ConfigurableEnvironment的

MutablePropertySources成员中,加载过程如下:


public class SpringApplication {
    ...
    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
          DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
       // 创建并配置环境,加载systemProperties, systemEnvironment, servletContextInitParams, servletConfigInitParams等配置
       ConfigurableEnvironment environment = getOrCreateEnvironment();
       configureEnvironment(environment, applicationArguments.getSourceArgs());
       ConfigurationPropertySources.attach(environment);
       // 加载application.properties, application.yml等应用配置,
       // 由EnvironmentPostProcessorApplicationListener#onApplicationEnvironmentPreparedEvent处理
       listeners.environmentPrepared(bootstrapContext, environment);
       DefaultPropertiesPropertySource.moveToEnd(environment);
       configureAdditionalProfiles(environment);
       bindToSpringApplication(environment);
       if (!this.isCustomEnvironment) {
          environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
       }
       ConfigurationPropertySources.attach(environment);
       return environment;
    }
      
    private ConfigurableEnvironment getOrCreateEnvironment() {
       if (this.environment != null) {
          return this.environment;
       }
       // 根据webApplicationType创建不同类型的Environment实例,
       // 以下3中类型的Environment都继承AbstractEnvironment,
       // 在其构造方法中customizePropertySources来加载不同来源的配置,
       // 比如 来源于System.getProperties()的systemProperties,来源于System.getenv()的systemEnvironment,
       switch (this.webApplicationType) {
       case SERVLET:
          return new StandardServletEnvironment();
       case REACTIVE:
          return new StandardReactiveWebEnvironment();
       default:
          return new StandardEnvironment();
       }
    }
  
    ...
}


2) SpringBoot配置存储

SpringBoot使用到的配置会加载到PropertySourcesPlaceholderConfigurer的MutablePropertySources成员中, 代码如下:


public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerSupport implements EnvironmentAware {
     
    // @Value 的配置存储
    @Nullable
    private MutablePropertySources propertySources;
 
 
    // @ConfigurationProperties 的配置存储,
    // 在postProcessBeanFactory方法中, 将propertySources赋值给了appliedPropertySources
    @Nullable
    private PropertySources appliedPropertySources;
     
    ...
}


 PropertySourcesPlaceholderConfigurer是一个BeanFactoryPostProcessor, 在IOC容器的启动过程中, 会主动调用其postProcessBeanFactory方法, 在此方法中完成了propertySources,appliedPropertySources的初始化, 代码如下:

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
   if (this.propertySources == null) {
      this.propertySources = new MutablePropertySources();
      if (this.environment != null) {
         // 以environment为配置源的environmentProperties
         this.propertySources.addLast(
            new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
               @Override
               @Nullable
               public String getProperty(String key) {
                  return this.source.getProperty(key);
               }
            }
         );
      }
      try {
         // 通过loadProperties(Properties props)加载本地资源文件作为属性源的localProperties
         PropertySource<?> localPropertySource =
               new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
         if (this.localOverride) {
            this.propertySources.addFirst(localPropertySource);
         }
         else {
            this.propertySources.addLast(localPropertySource);
         }
      }
      catch (IOException ex) {
         throw new BeanInitializationException("Could not load properties", ex);
      }
   }
   // 遍历处理bean属性
   processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
   // 将propertySources赋值给appliedPropertySources,为ConfigurationPropertiesBinder提供配置源
   this.appliedPropertySources = this.propertySources;
}
 
 
// 处理每个beanFactory中的每个BeanDefinition尝试替换 ${...} 属性.
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
      final ConfigurablePropertyResolver propertyResolver) throws BeansException {
   // 占位符设置
   propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
   propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
   propertyResolver.setValueSeparator(this.valueSeparator);
     
   // 定义StringValueResolver
   StringValueResolver valueResolver = strVal -> {
      // 通过propertyResolver获取配置的值
      String resolved = (this.ignoreUnresolvablePlaceholders ?
            propertyResolver.resolvePlaceholders(strVal) :
            propertyResolver.resolveRequiredPlaceholders(strVal));
      if (this.trimValues) {
         resolved = resolved.trim();
      }
      return (resolved.equals(this.nullValue) ? null : resolved);
   };
 
   // 通过doProcessProperties方法,将valueResolver添加到AbstractBeanFactory的embeddedValueResolvers中
   doProcessProperties(beanFactoryToProcess, valueResolver);
}


3) SpringBoot配置替换

Spring配置替换(配置生效)主要包括@Value配置替换和@ConfigurationProperties的配置替换。

3.1)@Value配置替换过程

SpringBoot 的启动过程包括2个重要的过程, 分别如下

  1. 扫描、解析BeanDefinition并注册到IOC容器中
  2. 根据BeanDefinition实例化Bean

@Value 是在实例化Bean的过程中完成属性注入的,主要流程如下:

AbstractAutowireCapableBeanFactory:     根据beanName, beanDefinition创建bean, 完成属性填充, 初始化等操作。

AutowiredAnnotationBeanPostProcessor:     装配bean中使用注解标注的成员变量,setter方法, 任意的配置方法。比较典型的是@Autowired注解和@Value注解。

InjectionMetadata: 遍历处理每个属性

AutowireCandidateResolver: 获取@Value注解中的值

StringValueResolver: 一个定义了处理字符串值的接口,只有一个接口方法resolveStringValue,可以用来解决占位符字符串。本文中的主要实现类在PropertySourcesPlaceholderConfigurer#processProperties方法中通过lamda表达式定义的。供ConfigurableBeanFactory类使用。

PropertySourcesPropertyResolver: 属性资源处理器,主要功能是获取PropertySources属性资源中的配置键值对。

PropertyPlaceholderHelper: 一个工具类,用来处理带有占位符的字符串。形如${name}的字符串在该工具类的帮助下,可以被用户提供的值所替代。


3.2) @ConfigurationProperties 替换过程

@ConfigurationProperties的加载是通过ConfigurationPropertiesBindingPostProcessor这个Bean后置处理器的bind方法完成的,具体流程如下:


ConfigurationPropertiesBindingPostProcessor核心代码如下:

private ApplicationContext applicationContext;
 
 
private ConfigurationPropertiesBinder binder;
 
 
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
   bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
   return bean;
}
 
 
private void bind(ConfigurationPropertiesBean bean) {
   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 {
      // 借助于ConfigurationPropertiesBinder.bind完成属性装载
      this.binder.bind(bean);
   }
   catch (Exception ex) {
      throw new ConfigurationPropertiesBindException(bean, ex);
   }
}

Binder#bindObject方法:
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
      Context context, boolean allowRecursiveBinding) {
   // 根据名称查找属性
   ConfigurationProperty property = findProperty(name, context);
   if (property == null && context.depth != 0 && containsNoDescendantOf(context.getSources(), name)) {
      return null;
   }
   AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
   if (aggregateBinder != null) {
      return bindAggregate(name, target, handler, context, aggregateBinder);
   }
   if (property != null) {
      try {
         // 绑定属性(属性替换)
         return bindProperty(target, context, property);
      }
      catch (ConverterNotFoundException ex) {
         // We might still be able to bind it using the recursive binders
         Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding);
         if (instance != null) {
            return instance;
         }
         throw ex;
      }
   }
   return bindDataObject(name, target, handler, context, allowRecursiveBinding);
}
 
 
private <T> Object bindProperty(Bindable<T> target, Context context, ConfigurationProperty property) {
   context.setConfigurationProperty(property);
   Object result = property.getValue();
   // placeholdersResolver是PropertySourcesPlaceholdersResolver的实例,
   // 其sources成员为PropertySourcesPlaceholderConfigurer的appliedPropertySources属性
   result = this.placeholdersResolver.resolvePlaceholders(result);
   result = context.getConverter().convert(result, target);
   return result;
}


2、BRCC介绍

介绍到这里,大家应该对Spring boot的配置使用与原理有比较清晰的了解了。相信此时大家也会有疑问出来,目前绝大数的应用都是分布式的架构下部署的,Springboot基于本地化的配置管理,成本高,变更难度大,风险也相对要高。

基于上面的问题与风险, BRCC就此诞生了, 它是百度基于大量的实际项目应用与实践的经验,经过多次的改进与升级,可以一站式的帮助大家解决配置开发与运维等众多需求,帮你一站式解决项目配置建设需求。



2.1 简介

  BRCC是一个分布式配置中心,用于统一管理应用服务的配置信息,避免各类资源散落在各个项目中,简化资源配置的维护成本。作为一种轻量级的解决方案,部署简单,同时支持多环境、多版本、多角色的资源管理,可以在不改变应用源码的情况下无缝切换和实时生效配置信息。

  BRCC由三部分组成:管理端、服务端、SDK,其中:
(1)管理端 : 前后端分离,后端基于Spring Boot 2.0开发,支持6个维度(产品、工程、环境、版本、分组、配置项)管理key-value格式的配置;支持细粒度的权限控制层级、操作轨迹等能力。安全易用,支持插件化的扩展轻松集成任何公司/组织的账号管理系统。
(2)服务端: 基于spring boot 2.0开发,打包后可以直接运行,支持配置的分发、更新推送。
(3)SDK端:支持java、go等多种开发语言和开发框架集成,支持spring注解、配置变更监听和刷新,零业务侵入性,低门槛集成(提供spring boot starter方式接入)。

2.2 特性

1. 统一管理不同环境、不同产品线的配置

  • 提供统一界面集中式管理不同环境、不同产品线、不同工程的配置
  • 通过版本的复制,可以高效的完成新业务的配置

2. 配置修改实时生效(热发布)

  • 默认2秒接收到最新的配置
  • 主动变更通知

3. 权限管理、角色隔离

  • 多级权限、多种角色细粒度管控(产品线、工程、环境),支持读写权限
  • 重要信息加密交互
  • Token机制

4. 多语言支持

  • 支持Java、Go客户端以SDK方式接入
  • 支持OpenAPI快速接入(不限定语言,只要支持Http协议即可)

5. 可追溯

  • 操作轨迹可追踪
  • 历史配置可查询和回溯

6. 缓存加持

  • 重要接口支持缓存开关配置

7. 更轻量

  • 部署简单,目前强依赖的外部服务是MySQL
  • 更聚焦:只关注配置相关的控制

8. 管理便捷

  • 细粒度化到key-value配置项层级
  • 支持模糊匹配检索
  • 快速导航

2.3 技术架构

3、SDK端使用

在线demo地址: https://github.com/baidu/brcc/tree/main/brcc-example

3.1 增加brcc依赖

创建一个通用的springboot应用,在pom文件中增加如下依赖:

<dependency>
    <groupId>com.baidu.mapp</groupId>
    <artifactId>brcc-sdk-starter</artifactId>
    <version>1.0.0</version>
</dependency>


brcc sdk starter的最好使用最新版本。brcc sdk starter

3.2 增加brcc的配置

在application.yml增加brcc的配置

rcc:
  cc-server-url: http://127.0.0.1:8080/ #这里brcc server的地址
  project-name: example                 #工程名
  cc-password: 123456                   #工程的api密码
  env-name: dev                         #环境名称
  cc-version-name: 1.0                  #版本名称
  log-properties: true                  #是否打印配置
  enable-update-callback: true          #是否启用自动更新

属性介绍

属性名称
默认值
描述

rcc.cc-server-url

null服务地址

rcc.project-name

null工程名称

rcc.cc-password

null
工程的api密码

rcc.env-name

null
环境名称

rcc.cc-version-name

null
版本名称

rcc.log-properties

false
是否打印配置

rcc.enable-update-callback

false
是否启用自动更新

rcc.connection-timeout

3000链接超时时间(ms)

rcc.read-timeout

10000读超时时间 (ms)

rcc.callback-interval

2000心跳探测频率(ms)


3.3 使用配置

通过spring的placeholder能力在各种注解中使用这些配置,如:

@Value("${a}")
int a = 0;
 
@Value("${b}")
long b = 0;
 
@Value("${c}")
String c;

3.4 自动刷新

当启动参数中rcc.enable-update-callback配置开启的时,自动更新功能打开。 自动更新采用的时观察者模式, 您只需要实现 com.baidu.brcc.ConfigItemChangedCallable接口,并将其以Bean的方式注册到Spring容器中, 当系统中有配置方式变更后,并且在管理平台上执行了推送变更操作, rcc会主动调用ConfigItemChangedCallable。

例如:

@Configuration
public class ExampleConfiguration {
 
    @Bean
    public ConfigItemChangedCallable configItemChangedCallable() {
        return new DefaultConfigItemChangedCallable();
    }
 
 
}
DefaultConfigItemChangedCallable 是rcc提供的默认变更回调实现, 其中仅仅打印变更日志,您可以实现自己的变更业务。

3.5 配置验证

启动日志中出现以下日志说明配置加载成功

配置发送变更后执行推送变更操作后,日志中会出现以下提示


4、BRCC管理端使用


安装部署

BRCC部署比较简单,详细请参考 https://github.com/baidu/brcc/blob/main/doc/deploy-guide.md

4.1、登录管理端

如何部署BRCC服务端,请移步https://github.com/baidu/brcc/blob/main/doc/deploy-guide.md, 我们假设已经在127.0.0.1的8080端口上部署了BRCC管理端,访问 http://127.0.0.1:8080/#/login,如下图:

登录界面


4.2、增加产品线

    产品线入口, 产品线入口有3个,首页中的【全部产品线】、具体产品,个人信息菜单下拉框中的【我的产品线】如下图:

产品线入口

进入产品线列表后,点击“新建产品线”按钮,新建产品线 test。

产品线新建 

新建后,点击test进入工程列表

4.3、增加工程

    进入工程列表后,点击"新建工程"按钮。

 工程管理

 新建工程 example,设置好api密码。新建后在工程列表界面点击"example"进入环境页面。

4.4、增加环境

点击"新增环境",增加一个新环境dev, 如下图:

 环境管理 

点击dev,进入版本页面。

4.5、增加版本

新增环境 1.0,点击"1.0"进入分组页面,如下图:

版本管理

4.6、增加分组

新增分组 g1,点击"g1"进入配置页面,如下图:

 分组管理

4.6、增加配置

增加3个配置:

a=5 b=34 c=xx13

点击“保存修改”按钮。

配置管理


5、总结

本文介绍了springboot的config原理并做了源码示例,讲解了@Value和@ConfigurationProperties的生效流程。brcc作为一款分布式配置中心,用于统一管理应用服务的配置信息,它完整兼容springboot的配置模型,可以直接使用@Value和@ConfigurationProperties。brcc提供配置实时生效和配置可追溯,在管理上支持多级权限多种纬度管理配置,并提供多种sdk和api使用和管理配置。


目前BRCC已经github上开源,大家可以访问获取全部代码 https://github.com/baidu/brcc, 也可以star我们项目,以便高效获得我们持续的更新信息.

我们也非常欢迎大家反馈问题与建议给我们:  https://github.com/baidu/brcc/issues 

如在使用中遇到问题,快速沟通,可微信扫描二维码,加入brcc技术交流群,添加下列管理员微信,并备注“brcc”,管理员邀您入群:



您也可以加入百度如流讨论群直接参与讨论和提问:3664772

posted on 2021-04-08 20:00 x.matthew 阅读(498) 评论(0)  编辑  收藏 所属分类: Spring|Hibernate|Other framework

只有注册用户登录后才能发表评论。


网站导航: