⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7332708703400312873 「我喺小VIE」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

引言

最近工作中常常感到后端 SpringBoot 项目里的属性配置十分混乱,在 SpringBoot 属性配置文件,数据库表,以及各个类中的属性字段都存在。因此,为了统一 SpringBoot 属性配置,尝试着以数据库作为统一的配置中心。

本文将从框架中属性配置存在的问题出发,分析并设计数据库表,之后改造属性的数据注入方式,最后在项目运行中完成验证可行性。

杂乱的属性配置

由于后台系统框架在设计之初没有对属性配置这一块做好统一规范,导致不同的人在着手不同业务的时候都用了各异的实现方式。随着涉及的业务越来越多,框架中属性配置这一块的问题也开始令人头大,配置臃肿的问题也终于到了该解决的时候。

首先概览一下目前 SpringBoot 项目中属性配置的存在情况,大致有三种形式:SpringBoot 属性配置文件、数据库配置表以及硬编码属性字段。

SpringBoot 属性配置文件

这类属性是配置在 SpringBoot 属性配置文件中的,部分配置还是十分敏感的数据,而且明文编写。

# 阿里云
aliyun.accessKeyId=LT***uao
aliyun.accessKeySecret=0Qhe***byr1f

数据库配置表

这类属性是读取数据库数据,之后加载到缓存或直接使用的。

硬编码属性字段

这类属性就是硬编码的常量,同样存在许多敏感数据。

public class WechatConfig {

public static final class MINI {
public static final String APP_ID = "wx2***dd5";
public static final String APP_SECRET = "38f***9c74";
}
}

配置分离方案

配置分离就是将属性配置与项目工程代码分开,这样不仅可以提高程序的健壮性和可维护性,还能使得属性配置更容易管理,做到不修改代码或重启程序的情况下进行配置更改。

当然,并不是所有的属性配置都需要配置分离,还需要先将它们归类再进一步分析。

属性配置归类

首先分析框架与项目中使用的配置项,并规范为三类:

  • 第一种是 Spring 配置,比如Spring的server.prot,Mybatis的mybatis.mapper-locations,即属于框架层面的,这种规定要配置在application[-ENV].properties;
  • 第二种是服务配置,比如定义的日志配置logback-spring.xml及配置logging.path,规定按原本方式单独配置;
  • 最后是第三方平台配置,比如阿里云OSS、微信小程序,规定使用数据库作为配置中心。

第一和第二种配置方式基本无需改动,因此不考虑对其实现配置分离;本次着重于第三方平台相关的配置,并统一以数据库作为配置中心。

数据表设计

在数据库表的设计中,一个属性配置最少由两个字段组成,也就是构成键值对的字段;同时我们可以设计一个分组字段,便于数据的分类和检索;最后,考虑到项目之后的扩展,可以考虑设计一个命名空间字段,用于配置环境区分等。

  • 字段data_key和data_value作为单项配置的键值对,其含义等同于Spring中的properties;
  • 字段data_group作为配置项的分组,例如对阿里云 OSS 的组名为aliyun.oss;
  • 字段namespace为命令空间的含义,可以作为区分环境用,或者分租户。

数据表结构如下:

CREATE TABLE `admin_data_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增Id',
`create_time` bigint(20) DEFAULT '0' COMMENT '创建时间',
`create_user` bigint(20) DEFAULT '0' COMMENT '创建者',
`update_time` bigint(20) DEFAULT '0' COMMENT '更新时间',
`update_user` bigint(20) DEFAULT '0' COMMENT '更新者',
`is_del` tinyint(4) DEFAULT '0' COMMENT '是否删除:1.是 0.否',
`data_key` varchar(100) DEFAULT '' COMMENT '键',
`data_value` varchar(100) DEFAULT '' COMMENT '值',
`description` varchar(50) DEFAULT '' COMMENT '描述',
`data_group` varchar(50) DEFAULT 'default_group' COMMENT '分组',
`namespace` varchar(50) DEFAULT 'public' COMMENT '命名空间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='配置中心';

配置中心表数据示例:

编码实现

核心配置类

核心配置类中使用@PostConstruct注解的init读取数据库数据配置,将读取到的数据配置作为MapPropertySource类型的数据源,之后将MapPropertySource放置到MutablePropertySources首位 (addFirst),作为最高优先级配置。

@Configuration
public class DBPropertyDataConfig {

@Resource
private ConfigurableEnvironment configurableEnvironment;

@Resource
private AdminDataConfigMapper adminDataConfigMapper;

@PostConstruct
public void init() {

MutablePropertySources mutablePropertySources = configurableEnvironment.getPropertySources();
List<AdminDataConfig> dataConfigList = adminDataConfigMapper.list(null);
if (dataConfigList == null || dataConfigList.size() == 0) {
return;
}
Map<String, Object> propertyMap = dataConfigList.stream()
.collect(Collectors.toMap(AdminDataConfig::getDataKey, AdminDataConfig::getDataValue));
MapPropertySource mapPropertySource = new MapPropertySource("db_data_config", propertyMap);
mutablePropertySources.addFirst(mapPropertySource);

}
}

数据注入

在应用新的数据库属性配置前还需要对原先的配置类改造,将字段或setter方法添加@Value注解使配置注入;同样地,对于新的属性配置则需要编写新的配置类。

以前面举例的WechatConfig配置类为例进行改造:

@Configuration
public class WechatConfig {

public static final class MINI {
public static String APP_ID;
public static String APP_SECRET;
}

@Value("${wechat.mini.app-id}")
public void setAppId(String appId) {
MINI.APP_ID = appId;
}
@Value("${wechat.mini.app-secret}")
public void setAppSecret(String appSecret) {
MINI.APP_SECRET = appSecret;
}
}

注意事项

配置类的注意事项:

  • 成员变量不可使用final声明
  • 使用Congfiguration将类作为Spring Bean
  • static声明的变量注入需要在setter方法上注解
  • 不能在内部类执行注入

关于Spring Bean的加载顺序的说明:

根据Spring Bean的加载顺序,要保证被注入Bean之前已加载DBPropertyDataConfig;例如,BlackBoxFacade会先于DBPropertyDataConfig加载,则需要@DependsOn指定其依赖关系。如下:

@Service
@DependsOn("DBPropertyDataConfig")
public class BlackBoxFacade {
@Value("${black-box.company}")
public String SYS_COMPANY;
}

测试验证

数据准备

准备数据库数据:

INSERT INTO `admin_data_config`
(`data_key`, `data_value`, `description`, `data_group`, `namespace`)
VALUES
('wechat.mini.app-id', 'wx2***', '微信小程序 appid', 'wechat.mini', 'public'),
('wechat.mini.app-secret', '38f***', '微信小程序 app secret', 'wechat.mini', 'public');

接口测试

  • 请求URL:http://localhost:8080/app/account/login
  • 请求方法:POST
  • Content-Type:form-data
  • 请求参数:data : {"jsCode":"FFF"}

断点观测

源代码设置断点:

断点数据观测:

总结与扩展

至此,以数据库作为配置中心的实现就已完成了。这个过程中整个的代码量其实只有核心的数据库配置读取这一点,更多的工作量还是在配置类的改造上。在测试验证通过之后,这个方案已逐渐应用在了线上的所有项目。

当然,以数据库作为配置中心只是一个选择,nacos同样也是一个成熟的可用方案,相比较来说,以数据库作为配置中心无需引入第三方也无需运行新服务,比较适合小型的项目。

最后,以数据库作为配置中心只是统一属性配置改造中的一小环,后面将会继续探索如何动态刷新属性配置,让整个属性配置更加灵活方便。

文章目录
  1. 1. 引言
  2. 2. 杂乱的属性配置
    1. 2.0.0.1. SpringBoot 属性配置文件
    2. 2.0.0.2. 数据库配置表
    3. 2.0.0.3. 硬编码属性字段
  • 3. 配置分离方案
    1. 3.0.0.1. 属性配置归类
  • 4. 数据表设计
  • 5. 编码实现
    1. 5.0.0.1. 核心配置类
    2. 5.0.0.2. 数据注入
    3. 5.0.0.3. 注意事项
  • 6. 测试验证
    1. 6.0.0.1. 数据准备
    2. 6.0.0.2. 接口测试
    3. 6.0.0.3. 断点观测
  • 7. 总结与扩展