XiaoLin's Blog

Xiao Lin

SpringCloudConfig 使用数据库方式实现配置存储

13
2024-02-04

介绍

Spring Cloud Config是一个基于Spring Boot的配置管理工具,它提供了集中式管理应用程序配置的方式,可以将应用程序配置存储在远程Git存储库、SVN存储库或本地文件系统中。使用Spring Cloud Config,可以实现多个服务之间的配置共享,避免了应用程序中硬编码的配置信息,可以动态地更新应用程序配置而无需重新启动应用程序。

Spring Cloud Config的优势如下:

  1. 集中式配置管理:通过Spring Cloud Config,可以将应用程序配置存储在一个地方,从而方便管理和维护。
  2. 动态配置更新:使用Spring Cloud Config,可以在不重启应用程序的情况下更新应用程序配置,实现应用程序的动态配置更新。
  3. 分布式系统的配置一致性:Spring Cloud Config提供了一种分布式配置管理的方式,可以确保多个服务的配置文件保持一致,避免了不同服务之间配置不一致的问题。
  4. 可扩展性:Spring Cloud Config支持自定义的扩展,可以根据需要实现自定义的配置存储方式和加密解密方式等。
  5. 安全性: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);
    }
}