spring_cloud有着强大的生态支持,其自带的分布式配置中心能够有效解决分布式环境中配置不统一的问题,提供一个中心化的配置中心。而且依靠其spring_bus(rabbitMq提供订阅)和github或者gitlab自带的webhook(钩子函数)能够实现将修改好后的配置push到远程git地址后,经过访问配置服务器的endPoints接口地址,即可将配置中心的变化推送到各个集群服务器中。java
Spring Cloud Config 是用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来链接配置仓库并为客户端提供获取配置信息、加密 / 解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们经过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。Spring Cloud Config 实现了对服务端和客户端中环境变量和属性配置的抽象映射,因此它除了适用于 Spring 构建的应用程序以外,也能够在任何其余语言运行的应用程序中使用。因为 Spring Cloud Config 实现的配置中心默认采用 Git 来存储配置信息,因此使用 Spring Cloud Config 构建的配置服务器,自然就支持对微服务应用配置信息的版本管理,而且能够经过 Git 客户端工具来方便的管理和访问配置内容。固然它也提供了对其余存储方式的支持,好比:SVN 仓库、本地化文件系统。git
话很少说,来看代码:github
首先本次采用的spring_cloud版本是:Finchley.RELEASE。spring_boot版本是2.0.3.RELEASE,低版本的spring_cloud并无actuator/bus-refresh这个endPoints接口地址,因此使用时要注意web
首先是配置中心服务器,须要如下4个引用:spring
<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>
其次是配置文件:json
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的配置若是不须要配置热更新是不须要写的bootstrap
启动类:服务器
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上去架构
接下来就是配置中心的客户端配置,本次准备了两个客户端,组成集群进行演示app
客户端须要的引用为:
<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
这样即可以进行配置文件远程修改后,无需启动客户端进行热加载了