介绍
Spring Cloud Config 是一个基于 Spring Boot 的配置管理工具,它提供了集中式管理应用程序配置的方式,可以将应用程序配置存储在远程 Git 存储库、SVN 存储库或本地文件系统中。使用 Spring Cloud Config,可以实现多个服务之间的配置共享,避免了应用程序中硬编码的配置信息,可以动态地更新应用程序配置而无需重新启动应用程序。
Spring Cloud Config 的优势如下:
- 集中式配置管理:通过 Spring Cloud Config,可以将应用程序配置存储在一个地方,从而方便管理和维护。
- 动态配置更新:使用 Spring Cloud Config,可以在不重启应用程序的情况下更新应用程序配置,实现应用程序的动态配置更新。
- 分布式系统的配置一致性:Spring Cloud Config 提供了一种分布式配置管理的方式,可以确保多个服务的配置文件保持一致,避免了不同服务之间配置不一致的问题。
- 可扩展性:Spring Cloud Config 支持自定义的扩展,可以根据需要实现自定义的配置存储方式和加密解密方式等。
- 安全性:Spring Cloud Config 支持基于 HTTPS 的安全传输和基于加密的配置存储,保证配置信息的安全性。
准备工作
数据库
数据库我们使用的是 postgresql
,ORM 框架使用的是 SpringDataJPA
- 表结构
DROP TABLE IF EXISTS "public"."infra_config_info";
CREATE TABLE "public"."infra_config_info" (
"id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"config_type" int4,
"content" text COLLATE "pg_catalog"."default" NOT NULL,
"create_time" timestamp(6),
"data_id" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"label" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"md5" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"profile" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"update_time" timestamp(6)
)
;
ALTER TABLE "public"."infra_config_info" OWNER TO "postgres";
-- ----------------------------
-- Uniques structure for table infra_config_info
-- ----------------------------
ALTER TABLE "public"."infra_config_info" ADD CONSTRAINT "idx_data_id_label_profile" UNIQUE ("data_id", "label", "profile");
-- ----------------------------
-- Primary Key structure for table infra_config_info
-- ----------------------------
ALTER TABLE "public"."infra_config_info" ADD CONSTRAINT "infra_config_info_pkey" PRIMARY KEY ("id");
-
config_type:文本文件数据格式类型如 YAML、Properties
-
content:配置内容文本
-
data_id:配置名称
-
label:分支 (dev,feature)
-
profile:环境 (dev,local,test)
-
实体类
package com.gs.config.environment.db.model;
import java.time.LocalDateTime;
import javax.persistence.*;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import com.gs.config.environment.db.enums.ConfigTypeEnum;
import com.gs.config.environment.db.model.convert.ConfigTypeEnumConverter;
import com.gs.config.environment.db.model.convert.LocalDateTimeConverter;
import lombok.Getter;
import lombok.Setter;
/**
*
* 配置详情DO
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/03/07
*
**/
@Table(name = "infra_config_info",
indexes = {@Index(name = "idx_data_id_label_profile", columnList = "data_id,label,profile", unique = true)})
@Entity
@Setter
@Getter
@EntityListeners(AuditingEntityListener.class)
public class ConfigInfoDO {
@Id
@GenericGenerator(name = "uuid_generator", strategy = "com.gs.config.environment.db.model.generator.UUIDGenerator")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "uuid_generator")
@Column(name = "id", length = 32, nullable = false)
private String id;
@Column(name = "data_id", nullable = false)
private String dataId;
@Column(name = "label", nullable = false)
private String label;
@Column(name = "content", columnDefinition = "text not null")
private String content;
@Column(name = "md5", nullable = false)
private String md5;
@Column(name = "profile", nullable = false)
private String profile;
@Column(name = "config_type")
@Convert(converter = ConfigTypeEnumConverter.class)
private ConfigTypeEnum configType;
@Column(name = "create_time")
@CreatedDate
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime createTime;
@Column(name = "update_time")
@LastModifiedDate
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime updateTime;
}
- repository
package com.gs.config.environment.db.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import com.gs.config.environment.db.enums.ConfigTypeEnum;
import com.gs.config.environment.db.model.ConfigInfoDO;
import com.gs.config.environment.db.model.ConfigInfoDO_;
/**
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/6
*
**/
public interface DbConfigRepository extends JpaRepository<ConfigInfoDO, Long>, JpaSpecificationExecutor<ConfigInfoDO> {
default ConfigInfoDO findConfigContent(String dataId, String label, String profile) {
final Optional<ConfigInfoDO> configDO = findOne((root, query, cb) -> {
return cb.and(cb.equal(root.get(ConfigInfoDO_.dataId), dataId),
cb.equal(root.get(ConfigInfoDO_.label), label), cb.equal(root.get(ConfigInfoDO_.profile), profile));
});
return configDO.orElse(null);
}
default boolean saveConfigContent(String dataId, String label, String profile, String content, String md5,
ConfigTypeEnum configType) {
final ConfigInfoDO configDO = new ConfigInfoDO();
configDO.setDataId(dataId);
configDO.setLabel(label);
configDO.setProfile(profile);
configDO.setContent(content);
configDO.setMd5(md5);
configDO.setConfigType(configType);
save(configDO);
return true;
}
default void updateConfigContent(String dataId, String label, String profile, String content, String md5) {
final Optional<ConfigInfoDO> configDO = findOne((root, query, cb) -> {
return cb.and(cb.equal(root.get(ConfigInfoDO_.dataId), dataId),
cb.equal(root.get(ConfigInfoDO_.label), label), cb.equal(root.get(ConfigInfoDO_.profile), profile));
});
if (configDO.isEmpty()) {
saveConfigContent(dataId, label, profile, content, md5, ConfigTypeEnum.YML);
} else {
configDO.get().setContent(content);
configDO.get().setMd5(md5);
save(configDO.get());
}
}
default void deleteConfigContent(String dataId, String label, String profile) {
final Optional<ConfigInfoDO> configDO = findOne((root, query, cb) -> {
return cb.and(cb.equal(root.get(ConfigInfoDO_.dataId), dataId),
cb.equal(root.get(ConfigInfoDO_.label), label), cb.equal(root.get(ConfigInfoDO_.profile), profile));
});
if (configDO.isPresent()) {
delete(configDO.get());
}
}
default void deleteConfigContentByGroupId(String dataId, String label) {
final Optional<ConfigInfoDO> configDO = findOne((root, query, cb) -> {
return cb.and(cb.equal(root.get(ConfigInfoDO_.dataId), dataId),
cb.equal(root.get(ConfigInfoDO_.label), label));
});
if (configDO.isPresent()) {
delete(configDO.get());
}
}
}
具体实现
数据读取实现
通过查看 SpringCloudConfig
官方的源码,参考 git 的数据方式。我们发现都是通过实现 *EnvironmentRepository
接口并重写 *findOne(String *application*, String *profile*, String *label*)
方法来达到任意数据源的配置读取,我们这次实现的是从数据库读取,所以我们定义一个名为 DbEnvironmentRepository
的类并实现 *EnvironmentRepository
接口 *
package com.gs.config.environment.db;
import java.util.Properties;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.core.Ordered;
import com.gs.config.environment.db.encrypt.ConfigTypeHandler;
import com.gs.config.environment.db.encrypt.ConfigTypeHandlerFactory;
import com.gs.config.environment.db.enums.ConfigTypeEnum;
import com.gs.config.environment.db.model.ConfigInfoDO;
import com.gs.config.environment.db.repository.DbConfigRepository;
/**
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/7
*
**/
public class DbEnvironmentRepository implements EnvironmentRepository, Ordered {
private final DbConfigRepository dbConfigRepository;
public DbEnvironmentRepository(DbConfigRepository dbConfigRepository) {
this.dbConfigRepository = dbConfigRepository;
}
@Override
public Environment findOne(String application, String profile, String label) {
final Environment environment = new Environment(application);
// 查询配置
final ConfigInfoDO configContent = dbConfigRepository.findConfigContent(application, profile, label);
if (configContent != null) {
// 通过配置文件类型,获取对应的处理工厂,处理成需要的properties
final ConfigTypeEnum configType = configContent.getConfigType();
final ConfigTypeHandler configTypeHandler = ConfigTypeHandlerFactory.getConfigTypeHandler(configType);
final Properties properties = configTypeHandler.handle(configContent.getContent());
environment.setProfiles(new String[] {profile});
environment.setLabel(label);
environment.setVersion("1.0.0");
environment.setState("1.0.0");
environment.add(new PropertySource(application, configTypeHandler.properties2Map(properties)));
}
return environment;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
ConfigTypeEnum
package com.gs.config.environment.db.enums;
import java.util.*;
import org.springframework.cloud.config.environment.PropertySource;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
/**
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/6
*
**/
@Getter
public enum ConfigTypeEnum {
YAML(0, "yaml"),
YML(1, "yml"),
PROPERTIES(2, "properties");
@JsonValue
private Integer code;
private String suffix;
ConfigTypeEnum(Integer code, String suffix) {
this.suffix = suffix;
this.code = code;
}
@JsonCreator
public static ConfigTypeEnum getByCode(Integer code) {
for (ConfigTypeEnum configTypeEnum : ConfigTypeEnum.values()) {
if (configTypeEnum.getCode().equals(code)) {
return configTypeEnum;
}
}
return null;
}
public static ConfigTypeEnum getBySuffix(String suffix) {
for (ConfigTypeEnum configTypeEnum : ConfigTypeEnum.values()) {
if (configTypeEnum.getSuffix().equals(suffix)) {
return configTypeEnum;
}
}
return null;
}
}
ConfigTypeHandler
package com.gs.config.environment.db.encrypt;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.encrypt.TextEncryptor;
/**
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/7
*
**/
public interface ConfigTypeHandler {
Properties handle(String content);
String getSuffix();
MediaType getContentType();
default String encrypt(String content, TextEncryptor textEncryptor) throws Exception {
final Properties properties = handle(content);
// 加密
final ConfigureEncryptor.ConfigureEncryptorBuilder builder = ConfigureEncryptor.builder();
builder.properties(properties);
builder.textEncryptor(textEncryptor);
final ConfigureEncryptor configureEncryptor = builder.build();
final Map<String, String> map = configureEncryptor.encryptMapping();
for (Map.Entry<String, String> stringStringEntry : map.entrySet()) {
content = content.replace(stringStringEntry.getKey(), stringStringEntry.getValue());
}
return content;
};
/**
* Properties无法兼容非String类型的值,所以需要转换
*
* @param properties 属性
* @return {@link Map}<{@link String}, {@link Object}>
*/
default Map<String, Object> properties2Map(Properties properties) {
// 创建一个HashMap对象
Map<String, Object> map = new LinkedHashMap<>();
// 遍历Properties的键集合
for (String key : properties.stringPropertyNames()) {
// 获取每个键对应的值,并存入HashMap中
map.put(key, properties.getProperty(key));
}
return map;
}
}
YamlConfigTypeHandler
package com.gs.config.environment.db.encrypt;
import java.util.Properties;
import org.springframework.beans.factory.config.YamlProcessor;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.MediaType;
/**
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/7
*
**/
public class YamlConfigTypeHandler implements ConfigTypeHandler {
private static final String SUFFIX = "yml";
private static final MediaType CONTENT_TYPE = MediaType.valueOf("text/yaml;charset=utf-8");
@Override
public Properties handle(String content) {
final YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean();
// 保证顺序
factoryBean.setDocumentMatchers((Properties properties) -> YamlProcessor.MatchStatus.FOUND);
factoryBean.setResources(new ByteArrayResource(content.getBytes()));
return factoryBean.getObject();
}
@Override
public String getSuffix() {
return SUFFIX;
}
@Override
public MediaType getContentType() {
// 获取content-type
return CONTENT_TYPE;
}
}
注入 SpringCloud 逻辑实现
我们通过实现 EnvironmentRepositoryFactory*<T extends EnvironmentRepository, P extends EnvironmentRepositoryProperties>
接口,并重写他的 build 方法,动态的构建我们 repository。
package com.gs.config.environment.db;
import org.springframework.cloud.config.server.environment.EnvironmentRepositoryFactory;
import com.gs.config.environment.db.repository.DbConfigRepository;
/**
* DbEnvironmentProperties为空继承,为满足他的泛型关系
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/7
*
**/
public class DbEnvironmentRepositoryFactory
implements EnvironmentRepositoryFactory<DbEnvironmentRepository, DbEnvironmentProperties> {
private final DbConfigRepository dbConfigRepository;
public DbEnvironmentRepositoryFactory(DbConfigRepository dbConfigRepository) {
this.dbConfigRepository = dbConfigRepository;
}
@Override
public DbEnvironmentRepository build(DbEnvironmentProperties dbEnvironmentProperties) throws Exception {
return new DbEnvironmentRepository(dbConfigRepository);
}
}
配置自定义数据源
package com.gs.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.config.server.encryption.TextEncryptorLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import com.gs.api.ConfigOperateController;
import com.gs.config.environment.db.DbEnvironmentProperties;
import com.gs.config.environment.db.DbEnvironmentRepository;
import com.gs.config.environment.db.DbEnvironmentRepositoryFactory;
import com.gs.config.environment.db.repository.DbConfigRepository;
import com.gs.config.environment.db.service.ConfigService;
import com.gs.config.environment.db.service.ConfigServiceImpl;
/**
*
* @author huangmuhong
* @version 1.0.0
* @date 2023/3/7
*
**/
@Profile("db")
@Configuration(proxyBeanMethods = false)
@Import({DataSourceAutoConfiguration.class, DbEnvironmentProperties.class})
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = "com.gs.config.environment.db.repository")
public class DbRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean(ConfigService.class)
public ConfigService configService(DbConfigRepository dbConfigRepository,
TextEncryptorLocator textEncryptorLocator) {
return new ConfigServiceImpl(dbConfigRepository, textEncryptorLocator);
}
@Bean
@ConditionalOnMissingBean(ConfigOperateController.class)
public ConfigOperateController configOperateController(ConfigService configService) {
return new ConfigOperateController(configService);
}
@Bean
@ConditionalOnMissingBean(DbEnvironmentRepository.class)
public DbEnvironmentRepository dbEnvironmentRepositoryFactory(DbConfigRepository dbConfigRepository,
DbEnvironmentProperties properties) throws Exception {
final DbEnvironmentRepositoryFactory factory = new DbEnvironmentRepositoryFactory(dbConfigRepository);
return factory.build(properties);
}
}
评论区