文章目录
- consul介绍
- 使用consul配置中心
- 将服务注册到consul上
- 推送配置到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的类或方法所引用的配置。是不是很高大上。
效果图
这里附上结果图: