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个重要的过程, 分别如下
- 扫描、解析BeanDefinition并注册到IOC容器中
- 根据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. 配置修改实时生效(热发布)
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的最好使用最新版本。
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