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的類或方法所引用的配置。是不是很高大上。
效果圖
這裡附上結果圖:
github原始碼: https://github.com/liaozihong/SpringCloud/tree/master/consul-key-value
參考連結:
https://www.cnblogs.com/EasonJim/p/7589546.html