SpringCloud利用Consul实现分布式配置中心

版权声明: https://blog.csdn.net/qq_39211866/article/details/82749605

consul介绍

Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案,Consul的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等)。使用起来也较 为简单。Consul使用Go语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与Docker等轻量级容器可无缝配合 。
consul安装起来也是非常简单,直接去官网下载对于系统的安装包即可。
windows下启动命令

consul.exe agent -dev

启动完毕,访问localhost:8500,即可看到consul的管理节目。当然你要用docker安装也可以!

使用consul配置中心

接下来,我就要用它来与Springboot结合,搭建分布式的公共配置中心。
首先,导入依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-all</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.cfg4j</groupId>
            <artifactId>cfg4j-consul</artifactId>
            <version>4.4.1</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

将服务注册到consul上

然后,需要配置bootstrap.yml。这里简单接受下这个配置文件。
其实yml和properties文件是一样的原理,主要是说明application和bootstrap的加载顺序。且一个项目上要么yml或者properties,二选一的存在。
Bootstrap.yml(bootstrap.properties)在application.yml(application.properties)之前加载,就像application.yml一样,但是用于应用程序上下文的引导阶段。它通常用于“使用Spring Cloud Config Server时,应在bootstrap.yml中指定spring.application.name和spring.cloud.config.server.git.uri”以及一些加密/解密信息。技术上,bootstrap.yml由父Spring ApplicationContext加载。父ApplicationContext被加载到使用application.yml的之前。
在本文中,需要从服务器加载“real”配置数据。为了获取URL(和其他连接配置,如密码等),需要一个较早的或“bootstrap”配置。因此,将配置服务器属性放在bootstrap.yml中,该属性用于加载实际配置数据(通常覆盖application.yml [如果存在]中的内容)。
这里贴出本例子的bootstrap.yml

# Server configuration
server:
  port: 8081
spring:
  application:
    name: test-consul
  # consul 配置
  cloud:
    consul:
      # consul服务器地址
      host: localhost
      # consul服务端口
      port: 8500
      config:
        # enabled为true表示启用配置管理功能
        enabled: true
        # watch选项为配置监视功能,主要监视配置的改变
        watch:
          enabled: true
          delay: 10000
          wait-time: 30
        # 表示如果没有发现配置,是否抛出异常,true为是,false为否,当为false时,consul会打印warn级别的日志信息
        fail-fast: false
        # 表示使用的配置格式
        format: key_value
        # 配置所在的应用目录名称
        prefix: config
        name: ${spring.application.name}
      # 服务发现配置
      discovery:
        # 启用服务发现
        enabled: true
        # 启用服务注册
        register: true
        # 服务停止时取消注册
        deregister: true
        # 表示注册时使用IP而不是hostname
        prefer-ip-address: true
        # 执行监控检查的频率
        health-check-interval: 30s
        # 设置健康检查失败多长时间后,取消注册
        health-check-critical-timeout: 30s
        # 健康检查的路径
        health-check-path: /actuator/info
        # 服务注册标识,格式为:应用名称+服务器IP+端口
        instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
logging:
  config: classpath:logback-develop.xml

上面的配置主要是指定consul服务地址,并且注册实例设置健康检查,并指定生成key-value形式和位置,相对较简单。

推送配置到consul配置中心

接着需要将配置注册到指定的配置中心上,这里提供一个配置类,可以根据指定的application.yml或.properties注册到配置中心上。配置类如下:

@Configuration
@RefreshScope
public class ConsulConfiguration {

    private static final Logger log = LoggerFactory.getLogger(ConsulConfiguration.class);
    @Autowired
    private ConsulClient consulClient;
    /**
     * 是否用本地配置覆盖consul远程配置,默认不覆盖, 覆盖: true / 不覆盖: false
     */
    @Value("${spring.cloud.consul.config.cover: false}")
    private Boolean cover;
    /**
     * key所在的目录前缀,格式为:config/应用名称/
     */
    @Value("#{'${spring.cloud.consul.config.prefix}/'.concat('${spring.cloud.consul.config.name}/')}")
    private String keyPrefix;
    /**
     * 加载配置信息到consul中
     *
     * @param key     配置的key
     * @param value   配置的值
     * @param keyList 在consul中已存在的配置信息key集合
     */
    private void visitProps(String key, Object value, List<String> keyList) {
        if (value.getClass() == String.class || value.getClass() == JSONArray.class) {
            // 覆盖已有配置
            if (cover) {
                this.setKVValue(key, value.toString());
            } else {
                if (keyList != null && !keyList.contains(key)) {
                    this.setKVValue(key, value.toString());
                }
            }
        } else if (value.getClass() == LinkedHashMap.class) {
            Map<String, Object> map = (LinkedHashMap) value;
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                visitProps(key + "." + entry.getKey(), entry.getValue(), keyList);
            }
        } else if (value.getClass() == HashMap.class) {
            Map<String, Object> map = (HashMap) value;
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                visitProps(key + "." + entry.getKey(), entry.getValue(), keyList);
            }
        }
    }


    /**
     * 封装配置信息到map中
     *
     * @param map 要封装的配置信息
     * @return 配置信息map
     */
    private Map<String, Object> formatMap(Map<String, Object> map) {
        Map<String, Object> newMap = new HashMap<>(16);
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            if (entry.getValue().getClass() == LinkedHashMap.class) {
                Map<String, Object> subMap = formatMap((Map<String, Object>) entry.getValue());
                newMap.put(entry.getKey(), subMap);
            } else if (entry.getValue().getClass() == ArrayList.class) {
                JSONArray jsonArray = new JSONArray((ArrayList) entry.getValue());
                newMap.put(entry.getKey(), jsonArray);
            } else {
                newMap.put(entry.getKey(), entry.getValue().toString());
            }
        }
        return newMap;
    }

    /**
     * 解析yml配置
     *
     * @param inputStream 要解析的yml文件输入流
     * @return 解析结果
     */
    private Map<String, Object> paserYml(InputStream inputStream) {
        Map<String, Object> newMap = new HashMap<>(16);
        try {
            Yaml yaml = new Yaml();
            Map map =  yaml.load(inputStream);
            newMap = formatMap(map);
        } catch (Exception e) {
            log.warn("解析Yml文件出现异常!");
        }
        return newMap;
    }

    /**
     * 启动时加载application.yml配置文件信息到consul配置中心
     * 加载到Consul的文件在ClassPathResource中指定
     */
    @PostConstruct
    private void init() {
        Map<String, Object> props = getProperties(null);
        List<String> keyList = this.getKVKeysOnly();
        log.info("Found keys : {}", keyList);
        for (Map.Entry<String, Object> prop : props.entrySet()) {
            //判断有spring.profiles.active则读取对应文件下的配置
            if (prop.getKey().equals("spring.profiles.active")) {
                Map<String, Object> props2 = getProperties((String) prop.getValue());
                for (Map.Entry<String, Object> prop2 : props2.entrySet()) {
                    visitProps(prop2.getKey(), prop2.getValue(), keyList);
                }
                continue;
            }
            visitProps(prop.getKey(), prop.getValue(), keyList);
        }
    }

    /**
     * 读取配置文件中的内容
     *
     * @param fixed
     * @return
     */
    private Map<String, Object> getProperties(String fixed) {
        PropertiesProviderSelector propertiesProviderSelector = new PropertiesProviderSelector(
                new PropertyBasedPropertiesProvider(), new YamlBasedPropertiesProvider(), new JsonBasedPropertiesProvider()
        );
        ClassPathResource resource;
        if (fixed != null && !fixed.isEmpty()) {
            resource = new ClassPathResource("application-" + fixed + ".properties");
        } else {
            resource = new ClassPathResource("application.properties");
        }
        String fileName = resource.getFilename();
        String path = null;
        Map<String, Object> props = new HashMap<>(16);
        try (InputStream input = resource.getInputStream()) {
            log.info("Found config file: " + resource.getFilename() + " in context " + resource.getURL().getPath());
            path = resource.getURL().getPath();
            if (fileName.endsWith(".properties")) {
                PropertiesProvider provider = propertiesProviderSelector.getProvider(fileName);
                props = (Map) provider.getProperties(input);

            } else if (fileName.endsWith(".yml")) {
                props = paserYml(resource.getInputStream());
            }
        } catch (IOException e) {
            log.warn("Unable to load properties from file: {},message: {} ", path, e.getMessage());
        }
        return props;
    }
    /**
     * 将应用的配置信息保存到consul中
     *
     * @param kvValue 封装的配置信息的map对象
     */

    public void setKVValue(Map<String, String> kvValue) {
        for (Map.Entry<String, String> kv : kvValue.entrySet()) {
            try {
                this.consulClient.setKVValue(keyPrefix + kv.getKey(), kv.getValue());
            } catch (Exception e) {
                log.warn("SetKVValue exception: {},kvValue: {}", e.getMessage(), kvValue);
            }
        }
    }

    public void setKVValue(String key, String value) {
        try {
            this.consulClient.setKVValue(keyPrefix + key, value);
        } catch (Exception e) {
            log.warn("SetKVValue exception: {},key: {},value: {}", e.getMessage(), key, value);
        }
    }

    /**
     * 获取应用配置的所有key-value信息
     *
     * @param keyPrefix key所在的目录前缀,格式为:config/应用名称/
     * @return 应用配置的所有key-value信息
     */

    public Map<String, String> getKVValues(String keyPrefix) {
        Map<String, String> map = new HashMap<>(16);

        try {
            Response<List<GetValue>> response = this.consulClient.getKVValues(keyPrefix);
            if (response != null) {
                for (GetValue getValue : response.getValue()) {
                    int index = getValue.getKey().lastIndexOf("/") + 1;
                    String key = getValue.getKey().substring(index);
                    String value = getValue.getDecodedValue();
                    map.put(key, value);
                }
            }
            return map;
        } catch (Exception e) {
            log.warn("GetKVValues exception: {},keyPrefix: {}", e.getMessage(), keyPrefix);
        }
        return null;
    }


    public Map<String, String> getKVValues() {
        return this.getKVValues(keyPrefix);
    }

    /**
     * 获取应用配置的所有key信息
     *
     * @param keyPrefix key所在的目录前缀,格式为:config/应用名称/
     * @return 应用配置的所有key信息
     */

    public List<String> getKVKeysOnly(String keyPrefix) {
        List<String> list = new ArrayList<>();
        try {
            Response<List<String>> response = this.consulClient.getKVKeysOnly(keyPrefix);

            if (response.getValue() != null) {
                for (String key : response.getValue()) {
                    int index = key.lastIndexOf("/") + 1;
                    String temp = key.substring(index);
                    list.add(temp);
                }
            }
            return list;
        } catch (Exception e) {
            log.warn("GetKVKeysOnly exception: {},keyPrefix: {}", e.getMessage(), keyPrefix);
        }
        return null;
    }

    public List<String> getKVKeysOnly() {
        return this.getKVKeysOnly(keyPrefix);
    }
}

个人习惯使用application.properties文件,如果是yml类型的自己更换上面涉及到的后缀。
可能有人会对@RefreshScope配置不解,它的作用是支持不停机动态刷新配置,也就是当注册中心的配置更改后,项目会感知到配置的变化,从而刷新有标记此注解的类或方法对配置的引用。
当然,前提是你还得开启定时调度注解,如下。

/**
 * Key value application
 * <p/>
 * Created in 2018.08.29
 * <p/>
 * 启用定时调度功能,Consul需要使用此功能来监控配置改变
 * @author Liaodashuai
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableScheduling
@EnableAutoConfiguration
public class ConsulKeyValueApplication {

    /**
     * The entry point of application.
     *
     * @param args the input arguments
     */
    public static void main(String[] args) {
        SpringApplication.run(ConsulKeyValueApplication.class, args);
    }
}

@EnableDiscoveryClient注解是将服务标记为客户端,可被发现注册并注册到consul上。
@EnableScheduling 开启定时调度功能,也就是隔一段时间回去扫描配置中心,如果配置有发生,有通知并刷新有标记@RefreshScope的类或方法所引用的配置。是不是很高大上。

效果图

这里附上结果图:
image

github源码: https://github.com/liaozihong/SpringCloud/tree/master/consul-key-value
参考链接:
https://www.cnblogs.com/EasonJim/p/7589546.html

猜你喜欢

转载自blog.csdn.net/qq_39211866/article/details/82749605