由于某些环境下大家可能使用Spring Cloud Config时不愿意使用GIT仓库等代码托管平台,我就给大家提供一下使用本地配置文件,修改文件保存后自动通知各个微服务进行配置刷新操作。本文基于Spring Boot使用2.0.3.RELEASE,Spring Cloud使用Finchley.SR1,大家都知道Spring boot2.0和1.0+有很多的变化,我也遇到了很多坑,在文章里我会尽量体现出来。好,接下来我们开始吧。

首先列出Config Server端代码:

服务端pom只需要依赖spring-cloud-config-server:

<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>2.5</version>
			<scope>compile</scope>
		</dependency>

其实这里有点偷懒,应该将cloud-bus模块加在服务器端,在后续由消息中间件的消息提供者这个角色放在服务端,由各个微服务作为消息消费端,这样设计比较合理,微服务有单一的职责,不会让某一个服务实例承担消息提供者的角色。本文是使用的Config Client端作为消息生产者。 还有个需要注意的地方,如果Config Server的pom依赖里或者集成自父工程的依赖里有spring-boot-devtools,需要排除掉,毕竟我们最后实现的目的是修改本地项目运行中配置文件实时修改并同步更新,如果不去掉热部署,文件修改的时候项目会自动重启。

服务端配置文件

server:
  port: 12788
eureka:
  client:
    service-url:
      defaultZone: http://localhost:12761/eureka
    instance:
      instance-id: demo-config-center
      prefer-ip-address: true
  server:
    wait-time-in-ms-when-sync-empty: 0
spring:
  cloud:
    config:
      server:
        native:
          search-locations: classpath:config/
  application:
    name: demo-config-center
  profiles:
    active: native

配置文件里比较重要的就是search-locations: classpath:config和active: native,前者是指定本地各个服务的配置文件存在的路径,后者是指定配置中心使用本地资源文件方式。

springcloudConfig配置YML中心动态刷新 springcloudconfig server自动刷新_数据库

本文中使用的微服务实例名为demo_portal,配置的结构就如图所示。-dev就是当前使用dev开发环境。 官方列举的匹配规则为:

springcloudConfig配置YML中心动态刷新 springcloudconfig server自动刷新_spring_02

我之前的服务实例名为demo-portal,但后面改为demo_portal,大家看出名字用短横线隔开会有什么问题吗,对,匹配规则也是用短横线来区分application和profile,会使匹配关系混乱,所以建议搭建用其他方式来命名实例名。

下面是存放在配置中心的客户端配置文件内容

server:
  port: 8088
spring:
  mvc:
    static-path-pattern: /**
  cloud:
    stream:
      kafka:
        binder:
          brokers: localhost:9092
  datasource:
    jdbc-url: jdbc:mysql://192.168.xx.xx/db
    username: Admin
    password: admin
config:
  test:
    value: hello world!
management:
  endpoints:
    web:
      exposure:
        include: bus-refresh

这里需要注意的地方:

1.本文使用的消息中间件为kafka,配置kafka的地址里只需要配置brokers里的kafka地址。很多资料里标记还需要配置zookeeper地址zkNodes属性,而且官方文档里也展示了属性:

springcloudConfig配置YML中心动态刷新 springcloudconfig server自动刷新_java_03

但是一旦配置这个属性会被提醒改属性已经被弃用,其实源码里也确实已被弃用

/** @deprecated */
@Deprecated
@DeprecatedConfigurationProperty(
	reason = "No longer necessary since 2.0"
)
public void setZkNodes(String... zkNodes) {
	this.zkNodes = zkNodes;
}

仔细分析一下,kafka启动的时候已经找到了需要依赖的zk地址,还有需要配置吗,所以官方文档应该是没有及时更新。

2.我们这里用到了datasource进行测试,因为后续我们就是要修改文件的数据库地址动态刷新连接的数据库。Spring boot2.0也对自定义的datasource也做了修改,如果不适用自定义的datasource可以使用url属性,但是需要将自定义datasource注入到spring时,会报错找不到“jdbc-url”属性,官方文档也有说明

springcloudConfig配置YML中心动态刷新 springcloudconfig server自动刷新_大数据_04

因此我们需要定义jdbc-url属性,后面的代码会讲到。

3.配置文件的自动刷新需要用到Spring boot的端点,如果不配置,默认没有开放供于刷新配置的接口,需开放bus-refresh。2.0之前是需要暴露出bus/refresh端口,目前改版后已经将端点都放在/actuator下了,下面是actuator-autoconfiguration包下的代码。

* @since 2.0.0
*/
@ConfigurationProperties(prefix = "management.endpoints.web")
public class WebEndpointProperties {
private final Exposure exposure = new Exposure();
/**
* Base path for Web endpoints. Relative to server.servlet.context-path or
* management.server.servlet.context-path if management.server.port is configured.
*/
private String basePath = "/actuator";
/**
* Mapping between endpoint IDs and the path that should expose them.
*/
private final Map<String, String> pathMapping = new LinkedHashMap<>();
public Exposure getExposure() {
return this.exposure;
}
public String getBasePath() {
return this.basePath;
}

所以我们调用的刷新接口为/actuator/bus-refresh,这个后面我们会看到。还有需要注意的一点是bus-refresh是集成了bus-kafka/bus-amqp后的暴露端点,其实可以只暴露actuator的refresh不用集成bus模块一样可以刷新,只不过没有消息中间件,而目前市场为了高可用性都会选择集群,不管是服务器端还是客户端,所以没有消息中间件是很难用的。

下面是我在服务端写的两个类

@Configuration
public class ConfigCenterConfiguration {
	@Bean
	public RestTemplate restTemplate(){
		return new RestTemplate();
	}
	@Bean(initMethod = "init")
	public ConfigFileModifyListener configFileModifyListener(){
		return new ConfigFileModifyListener();
	}
}

该类是Spring的配置类

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.ResourceUtils;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileNotFoundException;
@Slf4j
public class ConfigFileModifyListener extends FileAlterationListenerAdaptor {
	@Autowired
	private RestTemplate restTemplate;
	public void init(){
		log.info("ConfigFileModifyListener init...");
		FileAlterationObserver observer = null;
		try {
			observer = new FileAlterationObserver(new File(ResourceUtils.getFile("classpath:")
					.getPath() + File.separator + "config"));
		} catch (FileNotFoundException e) {
			log.error(e.getMessage());
		}
		observer.addListener(this);
		FileAlterationMonitor monitor = new FileAlterationMonitor(1000, observer);
		// 开始监控
		try{
			monitor.start();
			log.info("ConfigFileModifyListener start monitor...");
		}
		catch (Exception e){
			log.error(e.getMessage());
		}
	}
	@Override
	public void onFileChange(File file) {
		HttpHeaders headers = new HttpHeaders();
		MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
		headers.setContentType(type);
		headers.add("Accept", MediaType.APPLICATION_JSON.toString());
		HttpEntity<String> formEntity = new HttpEntity<String>(null, headers);
		String result = restTemplate.postForObject("http://127.0.0.1:8088/actuator/bus-refresh",formEntity,String.class);
		log.info("Bus-refresh post result : " + result);
	}
}

该类就是用来实现监听本地文件修改以及发送post请求通知微服务配置文件已经修改。 代码写的比较乱,并且有很多可以优化的地方,目前只做于测试。 @Slf4j使用了lombok插件,没有安装直接编译会出问题。 由于没有找到Spring有文件监控的框架,java7自带的文件修改监控会阻塞线程,最后使用了apache的common-io进行监控,性能和高可用性还没验证。

new File(ResourceUtils.getFile("classpath:")
            .getPath() + File.separator + "config")

对应的就是配置中心里各个服务存放配置文件的路径。

HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity<String> formEntity = new HttpEntity<String>(null, headers);
restTemplate.postForObject("http://127.0.0.1:8088/actuator/bus-refresh",formEntity,String.class)

对应的就是客户端开放出来的刷新端点地址。此处修改为服务端发送消息到kafka更为合理。 需要设置headers是因为bus-kafka模块里实现的rest接口采用json的格式,如果随便传会收到code 415的报错。 以上就是Config服务端的代码,一些简单的Spring boot启动类就没贴出来了。

接下来我们看看客户端的代码:

客户端pom依赖

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

jpa模块使用对象-关系映射查询数据库数据,bus-kafka和actuator共同实现配置动态刷新并有消息分发功能,config即配置中心的客户端。

客户端的bootstrap.yml配置文件:

spring:
  application:
    name: demo_portal
  cloud:
    config:
      discovery:
        enabled: true
        service-id: demo-config-center
      profile: dev
eureka:
  client:
    service-url:
      defaultZone: http://localhost:12761/eureka
    register-with-eureka: false

使用bootstrap.yml是因为启动优先级高于application.yml,我们客户端配置文件的内容不多,大多数和业务相关的已经放到了配置中心,并且是基本不需要改动的,目的也是为了让需要改动的配置统一管理的初衷。这里有个问题,在我们同时用eureka和配置中心的时候,有个鸡生蛋,蛋孵鸡的问题。要么我们指定eureka的地址,然后配置config server在eureka的实例(需要先启动eureka,在启动config server并注册到eureka,然后启动其他服务),要么我们使用uri来指定config server的地址(不依赖eureka,先启动config server,但启动会有很多找不到eruka的错误,有强迫症的应该受不了吧,只有等到eureka启动后,并自行重连后才能正常)。如果地址有变,我们至少都需要改一个。不可能服务实例启动的时候一个地址都不指定。所以选取哪种方式大家就自行选择吧。

下面就是客户端的JAVA代码

import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "oc_sys_user")
@Data
public class OcSysUser {
	@Id
	private Long id;
	private String name;
	private Integer sex;
}

我们目的是动态切换数据库,数据库查询操作是必须要的,那么实体类也是需要的。表结构就不展示了,比较简单就三个字段。

import com.demo.portal.entity.OcSysUser;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<OcSysUser,Integer> {
}

jpa模块的repository,用于操作数据库,jpa底层默认使用hibernate。

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
@Slf4j
public class DemoContextConfiguration {
	@Bean
	@ConfigurationProperties(prefix = "spring.datasource")
	@RefreshScope
	public DataSource dataSource(){
		return DataSourceBuilder.create().build();
	}
}

客户端的配置类,功能是就以spring.datasource为前缀的配置发生修改后,重新创建DataSource。比较重要的就是@RefreshScope,有些同学一直有疑问到底能刷新写什么东西,比如服务的端口改变了这个服务的占用端口就动态改变了吗?官方没有明确说这个端口的问题,但是文档还是表明了一些东西的:

springcloudConfig配置YML中心动态刷新 springcloudconfig server自动刷新_数据库_05

总而言之一句话,不管什么配置,不管配置是做什么的,能配置出什么东西。只要他是一个由Spring管理的Bean,只要配置了@RefreshScope,那么收到配置已经修改的消息后,Bean执行完当前任务就销毁,知道下次用户调用的时候,使用新的配置来初始化该Bean。所以能不能通过修改配置达到哪些目的,就看是否满足这两个条件。

import com.demo.portal.entity.OcSysUser;
import com.demo.portal.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ConfigTestController {
	@Autowired
	private UserRepository userRepository;
	@GetMapping("list")
	public List<OcSysUser> list(){
		return userRepository.findAll();
	}
}

用户展示数据库数据的Controller。

接下来我说下测试步骤吧,再启动所有服务后,访问客户端的list get请求,查到对应表的数据,然后去Config Server的运行目录Target下找到客户端配置文件demo_portal-dev.yml,打开修改数据库地址到其他数据库(但要保证另一个数据库也有相同的表),保存文件后,就可以看见服务端的控制台打出了发送post请求的日志,然后刷新刚才的list请求,就可以看见修改后的数据库的数据了。结果是比较简单的,就不贴图了。

到此为止就完成了,但是离生产环境需要的性能以及稳定性还差得比较远,大家需要调整,大家对本文有什么意见或者建议,或者本人理解有误的地方,希望提出来,谢谢。

by2018-8-21 纠正上述文章里的一个小问题: 上面提到微服务名用下划线将单词隔开避免配置文件名匹配时出问题,但在使用ribbon或者feign(底层也是使用了ribbon)时,会出现找不到服务的情况。官方给出的解答如下:

springcloudConfig配置YML中心动态刷新 springcloudconfig server自动刷新_spring_06

也就是说官方根据规范RFC2396 判断下划线并不属于合法的主机名命名规范。但目前没有找到官方推荐的微服务名命名规范,我暂时使用的驼峰命名,如果大家有什么好的建议希望可以提出来,谢谢。