1. 程式人生 > 實用技巧 >Spring Cloud Config

Spring Cloud Config

1 什麼是配置中心

1.1 配置中心概述

  • 對於傳統的單體應用而言,常使用配置檔案來管理所有配置,比如SpringBoot的application.yml檔案,但是在微服務架構中全部手動修改的話很麻煩而且不易維護。
  • 微服務的配置管理一般有以下需求:
    • 1️⃣集中配置管理,一個微服務架構中可能有成百上千個微服務,所以集中配置管理很重要。
    • 2️⃣不同環境不同配置:比如資料來源配置在不同環境(開發、測試、生產)中是不同的。
    • 3️⃣執行期間可動態調整。比如可以根據各個微服務的負載情況,動態調整資料來源連線池的大小等。
    • 4️⃣配置修改後可以自動更新。比如配置內容發生改變,微服務可以自動更新配置。
  • 綜上所述,對於微服務架構來說,一套統一的、通用的管理配置機制是不可缺少的重要組成部分。常見的做法就是通過配置伺服器進行管理。

1.2 常見的配置中心

1.2.1 Spring Cloud Config

  • Spring Cloud Config是分散式系統中的外部配置提供伺服器和客戶端支援。

1.2.2 Apollo(阿波羅)

  • Apollo(阿波羅)是攜程框架部門研發的分散式配置中心,能夠集中化管理應用不同環境、不同叢集的配置,配置修改後能夠實時推送到應用端,並且具備規範的許可權、流程治理等特性,適用於微服務配置管理場景。

1.2.3 Nacos

  • Nacos 致力於幫助您發現、配置和管理微服務。Nacos 提供了一組簡單易用的特性集,幫助您快速實現動態服務發現、服務配置、服務元資料及流量管理。

  • Nacos 幫助您更敏捷和容易地構建、交付和管理微服務平臺。 Nacos 是構建以“服務”為中心的現代應用架構 (例如微服務正規化、雲原生正規化) 的服務基礎設施。

2 Spring Cloud Config簡介

  • Spring Cloud Config專案是一個解決分散式系統的配置解決方案。它包含了Client和Server兩個部分,Server提供配置檔案的儲存,以介面的形式將配置檔案的內容提供出去;Client通過介面獲取資料,並依據此資料初始化自己的應用。

  • Spring Cloud Config為分散式系統的外部配置提供伺服器和客戶端支援。使用Config Server,您可以為所有環境中的應用程式管理其外部屬性。它非常適合Spring應用,也可以使用在其他語言的應用上。隨著應用程式通過從開發到測試和生產的部署流程,您可以管理這些環境之間的配置,並確定應用程式具有遷移時需要執行的一切。伺服器儲存後端的預設使用使用Git,因此他輕鬆的支援標籤版本的配置環境以及可以訪問用於管理內容的各種工具。
  • Spring Cloud Config服務端的特性:
    • 1️⃣Http,為外部配置提供基於資源的API(鍵值對或者等價的YAML內容)。
    • 2️⃣屬性值的加密和解密(對稱加密和非對稱加密)。
    • 3️⃣通過使用@EnableConfigServer在Spring Boot應用中非常簡單的嵌入。
  • Spring Cloud Config客戶端的 特性:
    • 1️⃣繫結Config服務端,並使用遠端的屬性源初始化Spring環境。
    • 2️⃣屬性值的加密和解密(對稱加密和非對稱加密)。

3 Spring Cloud Config入門

3.1 準備工作

  • Config Server是一個可橫向擴充套件、集中式的配置伺服器,它用於集中管理應用程式各個環境下的配置,預設使用Git儲存配置檔案內容,也可以使用SVN儲存(但是又有誰使用SVN呢?)或者是本地檔案儲存。
  • 在碼雲上建立倉庫:

  • 建立product-dev.ymlproduct-prod.yml檔案上傳到建立的倉庫上。
  • product-dev.yml:
server:
  port: 9008 # 微服務的埠號

spring:
  application:
    name: service-product # 微服務的名稱
  datasource:
    url: jdbc:mysql://192.168.1.57:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
  jpa:
    generate-ddl: true
    show-sql: true
    open-in-view: true
    database: mysql

# 配置 eureka
eureka:
  instance:
    # 主機名稱:服務名稱修改,其實就是向eureka server中註冊的例項id
    instance-id: service-product-dev:${server.port}
    # 顯示IP資訊
    prefer-ip-address: true
  client:
    service-url: # 此處修改為 Eureka Server的叢集地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  • product-prod.yml:
server:
  port: 9009 # 微服務的埠號

spring:
  application:
    name: service-product # 微服務的名稱
  datasource:
    url: jdbc:mysql://192.168.1.57:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
  jpa:
    generate-ddl: true
    show-sql: true
    open-in-view: true
    database: mysql

# 配置 eureka
eureka:
  instance:
    # 主機名稱:服務名稱修改,其實就是向eureka server中註冊的例項id
    instance-id: service-product-prod:${server.port}
    # 顯示IP資訊
    prefer-ip-address: true
  client:
    service-url: # 此處修改為 Eureka Server的叢集地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

檔案命名規則:

  • {application}-{profile}.yml
  • {application}-{profile}.properties
  • application為應用名稱,profile指的是開發環境(用於區分開發環境、測試環境和生產環境等)

3.2 搭建服務端程式

3.2.1 新建模組並匯入相關依賴

  • 修改部分:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
  • 完整部分:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring_cloud_demo</artifactId>
        <groupId>org.sunxiaping</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>config_server9010</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <!--   匯入Eureka Client對應的座標     -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

</project>

3.2.2 配置application.yml

server:
  port: 9010
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          # git服務地址
          uri: https://gitee.com/AncientFairy/config-repo.git
          # 配置git的使用者名稱
          username:
          # 配置git的密碼
          password:
          search-paths:
            - config-repo
      # 分支
      label: master


# 配置 eureka
eureka:
  instance:
    # 主機名稱:服務名稱修改,其實就是向eureka server中註冊的例項id
    instance-id: config-server:${server.port}
    # 顯示IP資訊
    prefer-ip-address: true
  client:
    service-url: # 此處修改為 Eureka Server的叢集地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

3.2.3 配置啟動類

package com.sunxiaping.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

/**
 * @author 許大仙
 * @version 1.0
 * @since 2020-10-09 16:48
 */
@SpringBootApplication
@EnableConfigServer //開啟配置中心功能
public class ConfigServer9010Application {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServer9010Application.class, args);
    }
}

3.2.4 測試

  • 啟動微服務,可以在瀏覽器,通過訪問http://localhost:9010/master/product-dev.yml請求訪問到Git伺服器上的檔案。

配置讀取規則(label:分支,name:服務名,profile:環境):

3.3 搭建客戶端程式

3.3.1 新建模組並匯入相關依賴

  • 修改部分:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
  • 完整部分:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring_cloud_demo</artifactId>
        <groupId>org.sunxiaping</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>config_client9011</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
</project>

3.3.2 配置bootstrap.yml

  • bootstrap.yml是系統級別的資源配置項,application.yml是使用者級別的資源配置項。

  • Spring Cloud會建立一個“Boostrap Context”,作為Spring應用的“Application Context”的父上下文。初始化的時候,“Boostrap Context”負載從外部源載入配置屬性並解析配置。這兩個上下文共享一個從外部獲取的“Environment”。

  • “Boostrap”屬性有高優先順序,預設情況下,不會覆蓋本地配置項。“Bootstrap Context”和“Application Context”有著不同的約定,所以新增了一個“bootstrap.yml”,保證“BootStrap Context”和“Application Context”配置的分離。

  • 要將Client模組下的application.yml改為bootstrap.yml,很關鍵。因為bootstrap.yml比application.yml優先載入。

spring:
  cloud:
    config:
      name: product # 應用名稱,需要對應git中配置檔名稱的前半部分
      profile: prod # 開發環境,需要對應git中配置檔名稱的後半部分
      label: master # 分支名稱
      uri: http://localhost:9010 # config-server的請求地址

3.3.3 實體類

package com.sunxiaping.product.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "tb_product")
public class Product implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "product_name")
    private String productName;

    @Column(name = "status")
    private Integer status;

    @Column(name = "price")
    private BigDecimal price;

    @Column(name = "product_desc")
    private String productDesc;

    @Column(name = "caption")
    private String caption;

    @Column(name = "inventory")
    private String inventory;

}

3.3.4 Dao層

package com.sunxiaping.product.dao;

import com.sunxiaping.product.domain.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {

}

3.3.5 Service層

  • ProductService.java
package com.sunxiaping.product.service;

import com.sunxiaping.product.domain.Product;

public interface ProductService {

    /**
     * 根據id查詢
     *
     * @param id
     * @return
     */
    Product findById(Long id);


    /**
     * 儲存
     *
     * @param product
     */
    void save(Product product);


    /**
     * 更新
     *
     * @param product
     */
    void update(Product product);


    /**
     * 刪除
     *
     * @param id
     */
    void delete(Long id);
}
  • ProductServiceImpl.java
package com.sunxiaping.product.service.impl;

import com.sunxiaping.product.dao.ProductRepository;
import com.sunxiaping.product.domain.Product;
import com.sunxiaping.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
@Transactional
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Product findById(Long id) {
        return this.productRepository.findById(id).orElse(new Product());
    }

    @Override
    public void save(Product product) {
        this.productRepository.save(product);
    }

    @Override
    public void update(Product product) {
        this.productRepository.save(product);
    }

    @Override
    public void delete(Long id) {
        this.productRepository.deleteById(id);
    }
}

3.3.6 Controller層

package com.sunxiaping.product.controller;

import com.sunxiaping.product.domain.Product;
import com.sunxiaping.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/product")
public class ProductController {

    @Value("${server.port}")
    private String port;

    @Value("${spring.cloud.client.ip-address}")
    private String ip;

    @Autowired
    private ProductService productService;

    @PostMapping(value = "/save")
    public String save(@RequestBody Product product) {
        this.productService.save(product);
        return "新增成功";
    }

    @GetMapping(value = "/findById/{id}")
    public Product findById(@PathVariable(value = "id") Long id) {
        Product product = this.productService.findById(id);
        product.setProductName("訪問的地址是:" + this.ip + ":" + this.port);
        return product;
    }
}

3.3.7 配置類

package com.sunxiaping.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient //開啟Eureka Client
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}

3.4 手動重新整理

3.4.1 概述

  • 我們已經在客戶端獲取到了配置中心的值,但是當我們修改git中的值的時候,服務端(Config Server)能實時的獲取到最新的值,但是客戶端(Config Client)讀取的是快取,無法實時獲取最新值。SpringCloud已經為我們解決了這個問題,那就是客戶端使用POST去觸發refresh,獲取最新資料,需要依賴spring-boot-starter-actuator。

3.4.2 應用示例

  • 匯入依賴:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 在對應的Controller類中新增@RefreshScope註解:
package com.sunxiaping.product.controller;

import com.sunxiaping.product.domain.Product;
import com.sunxiaping.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/product")
@RefreshScope //開啟動態重新整理
public class ProductController {

    @Value("${server.port}")
    private String port;

    @Value("${spring.cloud.client.ip-address}")
    private String ip;

    @Autowired
    private ProductService productService;

    @PostMapping(value = "/save")
    public String save(@RequestBody Product product) {
        this.productService.save(product);
        return "新增成功";
    }

    @GetMapping(value = "/findById/{id}")
    public Product findById(@PathVariable(value = "id") Long id) {
        Product product = this.productService.findById(id);
        product.setProductName("訪問的地址是:" + this.ip + ":" + this.port);
        return product;
    }
}
  • 在application.yml中開放端點配置:
spring:
  cloud:
    config:
      name: product # 應用名稱,需要對應git中配置檔名稱的前半部分
      profile: dev # 開發環境,需要對應git中配置檔名稱的後半部分
      label: master # 分支名稱
      uri: http://localhost:9010 # config-server的請求地址

# 暴露監控端點
management:
  endpoints:
    web:
      exposure:
        include: '*'
  • 測試:在Git伺服器上修改配置之後,需要使用curl -X POST "http://localhost:9008/actuator/refresh"傳送請求。

4 配置中心的高可用

4.1 概述

  • 在之前的程式碼中,客戶端都是直接呼叫配置中心的Server端來獲取配置資訊。這樣就存在了一個問題,客戶端和服務端的耦合性太高,如果Server要做叢集,客戶端只能通過原始的方式來路由,Server端改變IP地址的時候,客戶端也需要修改配置,不符合Spring Cloud 服務治理的概念。
  • Spring Cloud提供了這樣的解決方案,我們只需要將Server端當做一個服務註冊到Eureka中,Client端去Eureka中去獲取配置中心Server端的服務即可。

4.2 服務端改造

  • 匯入Eureka Client的依賴:
<!--   匯入Eureka Client對應的座標     -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • application.yml
server:
  port: 9010
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          # git服務地址
          uri: https://gitee.com/AncientFairy/config-repo.git
          # 配置git的使用者名稱
          username:
          # 配置git的密碼
          password:
          search-paths:
            - config-repo
      # 分支
      label: master


# 配置 eureka
eureka:
  instance:
    # 主機名稱:服務名稱修改,其實就是向eureka server中註冊的例項id
    instance-id: config-server:${server.port}
    # 顯示IP資訊
    prefer-ip-address: true
  client:
    service-url: # 此處修改為 Eureka Server的叢集地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

4.3 客戶端改造

  • bootstrap.yml:
spring:
  cloud:
    config:
      name: product # 應用名稱,需要對應git中配置檔名稱的前半部分
      profile: dev # 開發環境,需要對應git中配置檔名稱的後半部分
      label: master # 分支名稱
      #      uri: http://localhost:9010 # config-server的請求地址
      discovery: # 服務發現
        service-id: config-server
        enabled: true # 從Eureka中獲取配置資訊


# 配置 eureka
eureka:
  instance:
    # 主機名稱:服務名稱修改,其實就是向eureka server中註冊的例項id
    instance-id: service-product-dev:${server.port}
    # 顯示IP資訊
    prefer-ip-address: true
  client:
    service-url: # 此處修改為 Eureka Server的叢集地址
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

# 暴露監控端點
management:
  endpoints:
    web:
      exposure:
        include: '*'