1. 程式人生 > >SpringCloud利用Consul實現分散式配置中心

SpringCloud利用Consul實現分散式配置中心

文章目錄

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