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-Learning/tree/master/SpringCloud-Consul-Config-Server
參考鏈接:
https://www.cnblogs.com/EasonJim/p/7589546.html