简介
在微服务架构中,我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线。在总线上的各个实例都可以方便的广播一些需要让其他连接在该主题上的实例都知道的消息,例如配置信息的变更或者其他一些管理操作。
消息代理(Message Broker)是一种消息验证、传输、路由的架构模式。它在应用程序之间进行通信调度并最小化应用之间的依赖,使得应用程序可以高效地解耦通信过程。
消息代理是一个中间件产品,它的核心是一个消息的路由程序,用来实现接收和分发消息,并根据设定好的消息处理流来转发给正确的应用。它包括独立的通信和消息传递协议,可以在组织内部和组织之间进行通信。设计代理的目的就是为了能够从应用程序中传入消息,并执行一些特别的操作。
Config Server使用了Spring Cloud Bus后会对外提供一个HTTP接口叫做/bus-refresh,访问这个接口就会将最新的信息发送到Mq。那么谁访问这个接口呢,既然期望的是Git文件变更就发送消息 那么当然是由Git访问更加合适了。
什么时候用cloud bus
spring cloud bus在整个后端服务中起到联通的作用,联通后端的多台服务器。我们为什么需要他做联通呢?
后端服务器一般都做了集群化,很多台服务器,而且在大促活动期经常发生服务的扩容、缩容、上线、下线。这样,后端服务器的数量、IP就会变来变去,如果我们想进行一些线上的管理和维护工作,就需要维护服务器的IP。
比如我们需要更新配置、比如我们需要同时失效所有服务器上的某个缓存,都需要向所有的相关服务器发送命令,也就是调用一个接口。
你可能会说,我们一般会采用zookeeper的方式,统一存储服务器的ip地址,需要的时候,向对应服务器发送命令。这是一个方案,但是他的解耦性、灵活性、实时性相比消息总线都差那么一点。
总的来说,就是在我们需要把一个操作散发到所有后端相关服务器的时候,就可以选择使用cloud bus了。
开发步骤
手动刷新
1.Config服务-添加Bus的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
2.pom文件添加RabbitMq的配置
rabbitmq:
host: localhost
port: 5672
username: admin
password: 123456
3.启动配置,查看RabbitMq的Web页面,其中有Bus的队列
4.Order服务(Config的客户端)-同样引入依赖,pom文件配置RabbitMq,启动后RabbitMq多了一个队列
5.之前有写过接口可以访问Git配置文件的env属性,首先访问是dev,git中配置文件修改为dev2后访问还是dev
原因是之前说的需要访问下Config的/bus-refresh,但是Config是没这个接口的,所以需要在Config Server 的 pom文件中配置将这个接口暴露出来
# 允许/actuator/bus-refresh接口被外部调用
management:
endpoints:
web:
exposure:
include: "*"
6.然后在order服务 Controller上面添加@RefreshScope注解,@RefreshScope就是刷新范围,env的值是直接在这个Controller获取的,所以要刷新这个Controller
7.先访问Config Server的/actuator/bus-refresh接口去刷新配置(post),可以看到RabbitMq Config队列有了一条消息
重新访问 值发生改变
8.如果此时有两个Config Client 服务,而我们只想让其中一个Client刷新,那么需要在这个Client中配置:
spring.application.name=order
eureka.instance.instance-id=${spring.application.name}:${server.port}
然后在请求/bus/refresh时,后面添加参数/bus/refresh?destination=order:8081
配置自动刷新
1.首先配置Git Webhooks,其中URL必须是外网可以访问的,可以使用natapp.cn下载内网穿透工具
2.修改git文件就会访问Config Server的/actuator/bus-refresh接口,然后就刷新Order值发生修改。
但是呢,这个自动配置有个坑,就是在访问Config Server的/actuator/bus-refresh接口时候是会报400,原因:
因为GitHub在进行post请求的同时默认会在body加上一串载荷(payload)
{
"ref": "refs/heads/master",
"before": "c34de85d9488373f09b0d7a32c03c1cc43039bfa",
"after": "7f31e5860beb93f81376bd788c46906211b0b394",
"created": false,
"deleted": false,
"forced": false,
"base_ref": null,
"compare": "https://github.com/Tinysakura/practice-config-repo/compare/c34de85d9488...7f31e5860beb",
.........
}
于是我们的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
public class ConfigRequestWrapper extends HttpServletRequestWrapper{
public ConfigRequestWrapper(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();
}
};
}
}
@Component
public class ConfigFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse;
String url = new String(httpServletRequest.getRequestURI());
//只过滤/actuator/bus-refresh请求
if (!url.endsWith("/bus-refresh")) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//获取原始的body
String body = ReadAsChars(httpServletRequest);
//使用HttpServletRequest包装原始请求达到修改post请求中body内容的目的
ConfigRequestWrapper requestWrapper = new ConfigRequestWrapper(httpServletRequest);
filterChain.doFilter(requestWrapper, servletResponse);
}
/**
* 获取request中的Body
* @param request
* @return
*/
public String ReadAsChars(HttpServletRequest request)
{
BufferedReader br = null;
StringBuilder sb = new StringBuilder("");
try
{
br = request.getReader();
String str;
while ((str = br.readLine()) != null)
{
sb.append(str);
}
br.close();
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
if (null != br)
{
try
{
br.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
return sb.toString();
}
}