业务背景
在实际项目中,经常有关于配置的存储,将某个人的配置信息,或者某个公司的配置信息、某个系统的配置信息,存入库表记录中的需求。
不通用配置设计方式
一般的做法是建一张表,字段有:用户或系统/公司的主键id,具体的配置名称:比如是否给该用户推送消息,该用户使用的语言,该公司/系统是否采用夏令时,该公司/系统的下载文件路径等等,每种配置的类型(整型,枚举,布尔,字符串等等)不同,需要校验的方式也可能会不同。
表结构大致可能如下:
CREATE TABLE `user_config` (
`id` VARCHAR(64) NOT NULL COMMENT '主键id',
`account_id` VARCHAR(64) NOT NULL COMMENT '用户id',
`push_enable` BIT(1) NULL DEFAULT b'1' COMMENT '是否推送消息',
`language` INT(8) NOT NULL DEFAULT '0' COMMENT '语言类型,0-英语,1-中文,2-葡萄牙语',
PRIMARY KEY (`id`),
KEY `idx_account_id` (`account_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息配置表';
CREATE TABLE `company_config` (
`id` VARCHAR(64) NOT NULL COMMENT '主键id',
`company_id` VARCHAR(64) NOT NULL COMMENT '公司id',
`summer_time_enable` BIT(1) NULL DEFAULT b'0' COMMENT '是否开启夏令时',
`download_path` VARCHAR(256) NOT NULL DEFAULT 'D:/' COMMENT '下载路径',
PRIMARY KEY (`id`),
KEY `idx_company_id` (`company_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公司信息配置表';
这样设计的话,优势是简洁易懂。劣势是后续再有新的配置,表中需要增加新的字段,代码也相应要修改。
通用配置设计方式
本文介绍的是通用的配置设计方式,有一张配置字典表,字典表中记录各项配置的信息,另一张是用户配置信息表,记录着某个用户或某个公司的某项配置的值。
字典表的内容可以按照实际的业务增加更通用的字段,比如记录日志,是否需要权限校验等
表结构大致如下:
CREATE TABLE `config_dic` (
`id` varchar(64) NOT NULL COMMENT '主键id',
`config_key` varchar(32) NOT NULL COMMENT '配置名称,不能重复,唯一确定一项配置',
`config_type` varchar(8) NOT NULL COMMENT '配置类型',
`config_tip` varchar(128) DEFAULT NULL COMMENT '配置说明',
`default_value` varchar(256) NOT NULL COMMENT '配置的默认值',
`check_rule` varchar(256) DEFAULT NULL COMMENT '校验配置项值的规则',
`permission_enable` tinyint(4) DEFAULT '0' COMMENT '是否要校验权限',
`permission_value` varchar(256) DEFAULT NULL COMMENT '权限具体匹配的内容',
`operation_log` varchar(128) DEFAULT NULL COMMENT '不为空代表需要按该字段内容记录日志',
`create_time` bigint(20) unsigned NOT NULL COMMENT '创建时间(单位:毫秒)',
`update_time` BIGINT(20) DEFAULT NULL COMMENT '更新时间(单位:毫秒)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置字典表';
CREATE TABLE `config_info` (
`id` varchar(64) NOT NULL COMMENT '主键id',
`user_id` varchar(64) DEFAULT NULL COMMENT '用户或公司的id',
`config_value` varchar(256) NOT NULL COMMENT '配置值',
`config_id` varchar(64) NOT NULL COMMENT 'config_dic表主键id',
`create_time` BIGINT(20) unsigned NOT NULL COMMENT '创建时间(单位:毫秒)',
`update_time` BIGINT(20) unsigned NOT NULL COMMENT '更新时间(单位:毫秒)',
PRIMARY KEY (`id`),
KEY `idx_ccftenant_config_info_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户配置信息表';
这样设计的好处是,后面再拓展新的配置时,只需要将新的配置项插入到字典表中,无须改动代码。
数据示例
代码逻辑实现
数据库Entity类
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "config_dic")
public class ConfigDic {
@Id
@Column(name = "id", columnDefinition = "varchar(64)")
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "config_key", nullable = false, columnDefinition = "配置名称,不能重复,唯一确定一项配置")
private String configKey;
@Column(name = "config_type", nullable = false, columnDefinition = "配置类型")
private String configType;
@Column(name = "config_tip", columnDefinition = "配置说明")
private String configTip;
@Column(name = "default_value", nullable = false, columnDefinition = "配置的默认值")
private String defaultValue;
@Column(name = "check_rule", columnDefinition = "校验配置项值的规则")
private String checkRule;
@Column(name = "permission_enable", nullable = false, columnDefinition = "是否要校验权限")
private Boolean permissionEnable;
@Column(name = "permission_value", columnDefinition = "权限具体匹配的内容")
private String permissionValue;
@Column(name = "operation_log", columnDefinition = "不为空代表需要按该字段内容记录日志")
private String operationLog;
@Column(name = "create_time", columnDefinition = "创建时间戳")
private Long createTime;
@Column(name = "update_time", columnDefinition = "更新时间戳")
private Long updateTime;
}
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "config_info")
public class ConfigInfo {
@Id
@Column(name = "id", columnDefinition = "varchar(64)")
@GenericGenerator(name="idGenerator", strategy="uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "user_id", columnDefinition = "用户或公司的id")
private String userId;
@Column(name = "config_value", columnDefinition = "配置值")
private String configValue;
@Column(name = "config_id", columnDefinition = "ccftenant_config_dic表主键")
private String configId;
@Column(name = "create_time", columnDefinition = "创建时间")
private Long createTime;
@Column(name = "update_time", columnDefinition = "更新时间")
private Long updateTime;
}
DAO层
@Repository
public interface ConfigDicRepository extends JpaRepository<ConfigDic, Serializable> {
List<ConfigDic> findAllByConfigKeyIn(List<String> configKeys);
}
@Repository
public interface ConfigInfoRepository extends JpaRepository<ConfigInfo, Serializable> {
List<ConfigInfo> findAllByUserIdAndConfigIdIn(String userId, List<String> configIds);
}
service层
public interface ConfigService {
Map<String, Object> getConfigInfo(String userId, List<String> configKey);
void setConfigInfo(String userId, Map<String, Object> map);
}
@Service
public class ConfigServiceImpl implements ConfigService {
@Resource
private ConfigDicRepository configDicRepository;
@Resource
private ConfigInfoRepository configInfoRepository;
@Override
public Map<String, Object> getConfigInfo(String userId, List<String> configKey) {
Map<String, Object> res = new HashMap<>();
if (CollectionUtils.isEmpty(configKey)) {
return res;
}
// 查出指定配置字典表中数据
List<ConfigDic> configDics = configDicRepository.findAllByConfigKeyIn(configKey);
if (CollectionUtils.isEmpty(configDics)) {
return res;
}
// 查出用户配置表中,指定配置项+userid的配置
List<String> ids = configDics.stream().map(ConfigDic::getId).collect(Collectors.toList());
List<ConfigInfo> configInfos = configInfoRepository.findAllByUserIdAndConfigIdIn(userId, ids);
// 遍历配置,若用户配置表中存在,取用户配置表中值,否则使用默认值返回
Map<String, ConfigInfo> collect = configInfos.stream().collect(Collectors.toMap(ConfigInfo::getConfigId, a -> a, (k1, k2) -> k1));
configDics.forEach(c -> {
String value;
ConfigInfo configInfo = collect.get(c.getId());
if (configInfo != null) {
value = configInfo.getConfigValue();
} else {
value = c.getDefaultValue();
}
// 转回指定类型
Object targetValue = this.getTargetValue(value, c.getConfigType());
res.put(c.getConfigKey(), targetValue);
});
return res;
}
@Override
public void setConfigInfo(String userId, Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return;
}
Set<String> keySet = map.keySet();
List<String> configKey = new ArrayList(keySet);
// 查出指定配置字典表中数据
List<ConfigDic> configDics = configDicRepository.findAllByConfigKeyIn(configKey);
if (CollectionUtils.isEmpty(configDics)) {
return;
}
// 查出用户配置表中,指定配置项+userid的配置
List<String> ids = configDics.stream().map(ConfigDic::getId).collect(Collectors.toList());
List<ConfigInfo> configInfos = configInfoRepository.findAllByUserIdAndConfigIdIn(userId, ids);
// 遍历配置,进行校验,校验通过后,若用户配置表中存在,更新用户配置表中值,否则用户配置表中插入一条新数据
Map<String, ConfigInfo> collect = configInfos.stream().collect(Collectors.toMap(ConfigInfo::getConfigId, a -> a, (k1, k2) -> k1));
List<ConfigInfo> saveList = new ArrayList<>();
configDics.forEach(c -> {
if (c.getPermissionEnable()) {
// 校验权限,代码略
}
if (c.getOperationLog() != null) {
// 打印日志,代码略
}
// 数据类型校验
if (!this.checkDataType(map.get(c.getConfigKey()), c.getConfigType())) {
return;
}
// 数据规则校验
if (!this.checkDataRule(map.get(c.getConfigKey()), c.getConfigType(), c.getCheckRule())) {
return;
}
String saveValue = map.get(c.getConfigKey()).toString();
ConfigInfo configInfo = collect.get(c.getId());
if (configInfo != null) {
configInfo.setConfigValue(saveValue);
configInfo.setUpdateTime(System.currentTimeMillis());
} else {
configInfo = ConfigInfo.builder().userId(userId).configId(c.getId()).configValue(saveValue).createTime(System.currentTimeMillis()).updateTime(System.currentTimeMillis()).build();
}
saveList.add(configInfo);
});
configInfoRepository.saveAll(saveList);
}
private boolean checkDataType(Object value, String type) {
if (ObjectUtil.isNull(value) || ObjectUtil.isNull(type)) {
return false;
}
String valueType = this.getValueType(value);
return valueType.equals(type);
}
private Object getTargetValue(String value, String type) {
boolean nullOrEmpty = StringUtils.isEmpty(value);
if (Double.class.getSimpleName().equals(type)) {
return nullOrEmpty ? 0 : Double.parseDouble(value);
} else if (Long.class.getSimpleName().equals(type)) {
return nullOrEmpty ? 0 : Long.parseLong(value);
} else if (Integer.class.getSimpleName().equals(type)) {
return nullOrEmpty ? 0 : Integer.parseInt(value);
} else if (Float.class.getSimpleName().equals(type)) {
return nullOrEmpty ? 0 : Float.parseFloat(value);
} else if (Short.class.getSimpleName().equals(type)) {
return nullOrEmpty ? 0 : Short.parseShort(value);
} else if (Boolean.class.getSimpleName().equals(type)) {
return nullOrEmpty ? 0 : Boolean.parseBoolean(value);
} else {
return nullOrEmpty ? "" : value;
}
}
public static String getValueType(Object value) {
if (ObjectUtil.isNull(value)) {
return String.class.getSimpleName();
}
if (value instanceof Double) {
return Double.class.getSimpleName();
} else if (value instanceof Long) {
return Long.class.getSimpleName();
} else if (value instanceof Integer) {
return Integer.class.getSimpleName();
} else if (value instanceof Float) {
return Float.class.getSimpleName();
} else if (value instanceof Short) {
return Short.class.getSimpleName();
} else if (value instanceof Boolean) {
return Boolean.class.getSimpleName();
} else {
return String.class.getSimpleName();
}
}
public boolean checkRule(Object value, String valueType, String rule) {
if (StringUtils.isBlank(rule)) {
return true;
}
Rule ruleObj = JSON.parseObject(rule, Rule.class);
if (ObjectUtil.isNull(ruleObj)) {
return false;
}
if (Double.class.getSimpleName().equals(valueType)) {
Double v = (Double) value;
if (ObjectUtil.isNotNull(ruleObj.getMin()) && v < ruleObj.getMin()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getMax()) && v > ruleObj.getMax()) {
return false;
}
} else if (Long.class.getSimpleName().equals(valueType)) {
Long v = (Long) value;
if (ObjectUtil.isNotNull(ruleObj.getMin()) && v < ruleObj.getMin()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getMax()) && v > ruleObj.getMax()) {
return false;
}
} else if (Integer.class.getSimpleName().equals(valueType)) {
Integer v = (Integer) value;
if (ObjectUtil.isNotNull(ruleObj.getMin()) && v < ruleObj.getMin()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getMax()) && v > ruleObj.getMax()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getRange()) && !ruleObj.getRange().contains(v)) {
return false;
}
} else if (Float.class.getSimpleName().equals(valueType)) {
Float v = (Float) value;
if (ObjectUtil.isNotNull(ruleObj.getMin()) && v < ruleObj.getMin()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getMax()) && v > ruleObj.getMax()) {
return false;
}
} else if (Short.class.getSimpleName().equals(valueType)) {
Short v = (Short) value;
if (ObjectUtil.isNotNull(ruleObj.getMin()) && v < ruleObj.getMin()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getMax()) && v > ruleObj.getMax()) {
return false;
}
if (ObjectUtil.isNotNull(ruleObj.getRange()) && !ruleObj.getRange().contains(v)) {
return false;
}
} else if (String.class.getSimpleName().equals(valueType)) {
String v = (String) value;
if (ObjectUtil.isNotNull(ruleObj.getLength()) && v.length() > ruleObj.getLength()) {
return false;
}
}
return true;
}
private boolean checkDataRule(Object value, String type, String rule) {
if (ObjectUtil.isNull(value) || ObjectUtil.isNull(type)) {
return false;
}
if (StringUtils.isBlank(rule)) {
return true;
}
return this.checkRule(value, type, rule);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Rule {
private Long min;
private Long max;
private List<Integer> range;
private Integer length;
}
}
controller层
@RestController
@RequestMapping("/config")
public class ConfigController {
@Resource
private ConfigService configService;
@PostMapping(value = "/v1/get")
public Map<String, Object> getConfigInfo(@RequestBody GetConfigVo getConfigVo, BindingResult result) {
return configService.getConfigInfo(getConfigVo.getUserId(), getConfigVo.getList());
}
@PostMapping(value = "/v1/set")
public void setConfigInfo(@RequestBody SetConfigVo setConfigVo, BindingResult result) {
configService.setConfigInfo(setConfigVo.getUserId(), setConfigVo.getMap());
}
}
测试结果
运行程序,使用postman调接口进行测试
获取配置
设置配置