1. 程式人生 > >spring_cloud config 配置中心及利用Github實現自動化熱載入配置

spring_cloud config 配置中心及利用Github實現自動化熱載入配置

    spring_cloud有著強大的生態支援,其自帶的分散式配置中心可以有效解決分散式環境中配置不統一的問題,提供一箇中心化的配置中心。並且依靠其spring_bus(rabbitMq提供訂閱)和github或者gitlab自帶的webhook(鉤子函式)可以實現將修改好後的配置push到遠端git地址後,通過訪問配置伺服器的endPoints介面地址,便可將配置中心的變化推送到各個叢集伺服器中。

    Spring Cloud Config 是用來為分散式系統中的基礎設施和微服務應用提供集中化的外部配置支援,它分為服務端與客戶端兩個部分。其中服務端也稱為分散式配置中心,它是一個獨立的微服務應用,用來連線配置倉庫併為客戶端提供獲取配置資訊、加密 / 解密資訊等訪問介面;而客戶端則是微服務架構中的各個微服務應用或基礎設施,它們通過指定的配置中心來管理應用資源與業務相關的配置內容,並在啟動的時候從配置中心獲取和載入配置資訊。Spring Cloud Config 實現了對服務端和客戶端中環境變數和屬性配置的抽象對映,所以它除了適用於 Spring 構建的應用程式之外,也可以在任何其他語言執行的應用程式中使用。由於 Spring Cloud Config 實現的配置中心預設採用 Git 來儲存配置資訊,所以使用 Spring Cloud Config 構建的配置伺服器,天然就支援對微服務應用配置資訊的版本管理,並且可以通過 Git 客戶端工具來方便的管理和訪問配置內容。當然它也提供了對其他儲存方式的支援,比如:SVN 倉庫、本地化檔案系統。

    話不多說,來看程式碼:

首先本次採用的spring_cloud版本是:Finchley.RELEASE。spring_boot版本是2.0.3.RELEASE,低版本的spring_cloud並沒有actuator/bus-refresh這個endPoints介面地址,所以使用時要注意

首先是配置中心伺服器,需要以下4個引用:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>

其次是配置檔案:

server.port=20000
#服務的git倉庫地址
spring.cloud.config.server.git.uri=https://github.com/narutoform/springCloudConfig
#配置檔案所在的目錄
spring.cloud.config.server.git.search-paths=/**
#配置檔案所在的分支
spring.cloud.config.label=master
#git倉庫的使用者名稱
spring.cloud.config.username=narutoform
#git倉庫的密碼
spring.cloud.config.password=*****
spring.application.name=springCloudConfigService
eureka.client.service-url.defaultZone=http://localhost:10000/eureka
eureka.instance.preferIpAddress=true
#rabbitmq
spring.rabbitmq.host=192.168.210.130
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.publisher-confirms=true
management.endpoints.web.exposure.include=bus-refresh

其中要注意將bus-refresh介面開啟,並且使用者名稱和密碼只有訪問需要許可權的專案是才需要,例如gitlab,但github是不需要的,此外rabbitMq的配置如果不需要配置熱更新是不需要寫的

啟動類:

package cn.chinotan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableConfigServer
@EnableEurekaClient
@ServletComponentScan
public class StartConfigServerEureka {

    public static void main(String[] args) {
        SpringApplication.run(StartConfigServerEureka.class, args);
    }

}

需要將此配置中心註冊到euerka上去

接下來就是配置中心的客戶端配置,本次準備了兩個客戶端,組成叢集進行演示

客戶端需要的引用為:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>

配置檔案為:bootstrap.yml

#開啟配置服務發現
spring.cloud.config.discovery.enabled: true
spring.cloud.config.enabled: true
#配置服務例項名稱
spring.cloud.config.discovery.service-id: springCloudConfigService
#配置檔案所在分支
spring.cloud.config.label: master
spring.cloud.config.profile: prod
#配置服務中心
spring.cloud.config.uri: http://localhost:20000/
eureka.client.service-url.defaultZone: http://localhost:10000/eureka
eureka.instance.preferIpAddress: true
management.endpoints.web.exposure.include: bus-refresh

注意配置中心必須寫到bootstrap.yml中,因為bootstrap.yml要先於application.yml讀取

下面是application.yml配置

server.port: 40000
spring.application.name: springCloudConfigClientOne
#rabbitmq
spring.rabbitmq.host: 192.168.210.130
spring.rabbitmq.port: 5672
spring.rabbitmq.username: guest
spring.rabbitmq.password: guest
spring.rabbitmq.publisher-confirms: true

注意客戶端如果要熱更新也需要引入spring_bus相關配置和rabbitmq相關配置,開啟bus-refresh接口才行,客戶端不需要輸入遠端git的地址,只需從剛剛配置好的伺服器中讀取就行,連線時需要配置配置伺服器的erruka的serverId,本文中是springCloudConfigService,此外還可以指定label(分支)和profile(環境)

在配置中心伺服器啟動好後便可以啟動客戶端來讀取伺服器取到的配置

客戶端啟動如下:

可以看到客戶端在啟動時會去配置中心伺服器去取伺服器從遠端git倉庫取到的配置

在客戶端中加入如下程式碼,便可以直接讀取遠端配置中心的配置了

package cn.chinotan.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope
public class ConfigClientController {

    @Value("${key}")
    private String key;

    @GetMapping("/key")
    public String getProfile() {
        return this.key;
    }
}

遠端配置中心結構為:

 

要注意客戶端需要在你希望改變的配置中加入@RefreshScope才能夠進行配置的熱更新,否則訂閱的客戶端不知道將哪個配置進行更新

此外客戶端訪問的那個地址,也可以get直接訪問,從而判斷配置中心伺服器是否正常啟動

通過訪問http://localhost:20000/springCloudConfig/default介面就行

證明配置服務中心可以從遠端程式獲取配置資訊。

http請求地址和資原始檔對映如下:

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
現在我們在客戶端上訪問之前寫的那個controller來得到配置檔案中的配置

可見客戶端能夠從伺服器拿到遠端配置檔案中的資訊

其實客戶端在啟動時便會通過spring_boot自帶的restTemplate發起一個GET請求,從而得到伺服器的資訊,原始碼如下:

private Environment getRemoteEnvironment(RestTemplate restTemplate,
			ConfigClientProperties properties, String label, String state) {
		String path = "/{name}/{profile}";
		String name = properties.getName();
		String profile = properties.getProfile();
		String token = properties.getToken();
		int noOfUrls = properties.getUri().length;
		if (noOfUrls > 1) {
			logger.info("Multiple Config Server Urls found listed.");
		}

		Object[] args = new String[] { name, profile };
		if (StringUtils.hasText(label)) {
			if (label.contains("/")) {
				label = label.replace("/", "(_)");
			}
			args = new String[] { name, profile, label };
			path = path + "/{label}";
		}
		ResponseEntity<Environment> response = null;

		for (int i = 0; i < noOfUrls; i++) {
			Credentials credentials = properties.getCredentials(i);
			String uri = credentials.getUri();
			String username = credentials.getUsername();
			String password = credentials.getPassword();

			logger.info("Fetching config from server at : " + uri);

			try {
				HttpHeaders headers = new HttpHeaders();
				addAuthorizationToken(properties, headers, username, password);
				if (StringUtils.hasText(token)) {
					headers.add(TOKEN_HEADER, token);
				}
				if (StringUtils.hasText(state) && properties.isSendState()) {
					headers.add(STATE_HEADER, state);
				}

				final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
				response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
						Environment.class, args);
			}
			catch (HttpClientErrorException e) {
				if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
					throw e;
				}
			}
			catch (ResourceAccessException e) {
				logger.info("Connect Timeout Exception on Url - " + uri
						+ ". Will be trying the next url if available");
				if (i == noOfUrls - 1)
					throw e;
				else
					continue;
			}

			if (response == null || response.getStatusCode() != HttpStatus.OK) {
				return null;
			}

			Environment result = response.getBody();
			return result;
		}

		return null;
	}

之後,我們試試配置檔案熱更新

我們在啟動伺服器和客戶端是,會發現,rabbitMq多了一個交換機和幾個佇列,spring_bus正是通過這這個topic交換機來進行變更配置的通知個推送的,效果如下:

在更改遠端配置檔案後,呼叫配置伺服器的http://localhost:20000/actuator/bus-refresh介面後:

可以看到,進行了訊息傳遞,將變化的結果進行了推送

 

其中呼叫http://localhost:20000/actuator/bus-refresh是因為伺服器在啟動時暴露出來了這個介面

可以看到這個是一個POST請求,而且其介面在呼叫之後什麼也不返回,而且低版本spring_cloud中沒有這個介面

這樣是可以實現了客戶端叢集熱更新配置檔案,但是還的手動呼叫http://localhost:20000/actuator/bus-refresh介面,有什麼辦法可以在遠端配置倉庫檔案更改後自動進行向客戶端推送呢,答案是通過github或者是gitlab的webhook(鉤子函式)進行,開啟gitHub的管理介面可以看到如下資訊,點選add webhook進行新增鉤子函式

由於我沒有公網地址,只能通過內網穿透進行埠對映,使用的是ngrok進行的

 

這樣便可以通過http://chinogo.free.idcfengye.com這個公網域名訪問到我本機的服務了

但是這樣就可以了嗎,還是太年輕

可以看到GitHub在進行post請求的同時預設會在body加上這麼一串載荷(payload)

還沒有取消傳送載荷的功能,於是我們的spring boot因為無法正常反序列化這串載荷而報了400錯誤:

Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.String` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_ARRAY token

於是自然而然的想到修改body為空來避免json發生轉換異常,開始修改body,於是去HttpServletRequest中去尋找setInputStream方法,servlet其實為我們提供了一個HttpServletRequestMapper的包裝類,我們通過繼承該類重寫getInputStream方法返回自己構造的ServletInputStream即可達到修改request中body內容的目的。這裡為了避免節外生枝我直接返回了一個空的body。
自定義的wrapper類

package cn.chinotan.config;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;

/**
 * @program: test
 * @description: 過濾webhooks,清空body
 * @author: xingcheng
 * @create: 2018-10-14 17:56
 **/
public class CustometRequestWrapper extends HttpServletRequestWrapper {

    public CustometRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        byte[] bytes = new byte[0];
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.read() == -1 ? true:false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }
}

實現過濾器

package cn.chinotan.config;

import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @program: test
 * @description: 過濾器
 * @author: xingcheng
 * @create: 2018-10-14 17:59
 **/
@WebFilter(filterName = "bodyFilter", urlPatterns = "/*")
@Order(1)
public class MyFilter implements Filter {
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;

        String url = new String(httpServletRequest.getRequestURI());

        //只過濾/actuator/bus-refresh請求
        if (!url.endsWith("/bus-refresh")) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        //使用HttpServletRequest包裝原始請求達到修改post請求中body內容的目的
        CustometRequestWrapper requestWrapper = new CustometRequestWrapper(httpServletRequest);

        filterChain.doFilter(requestWrapper, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

別忘了啟動類加上這個註解:

@ServletComponentScan

這樣便可以進行配置檔案遠端修改後,無需啟動客戶端進行熱載入了