11-微服务技术组件
一、使用Nacos实现集中式配置管理
(一)配置中心模型
在微服务架构中,存在着多环境、多服务、多实例(集群化)的情况,那么就需要将一些配置信息集中的放在一个地方做统一管理,这就是配置中心的原型。
对于配置中心来说,要保证其隔离性、一致性、安全性和易管理性,隔离性是指如果有多个环境的配置,相互之间要保证数据隔离;一致性是指集群部署的配置中心,更改其中一个实例的配置,其他实例要保证同步修改;安全性是指要保证数据安全性,防止数据泄露。
配置中心一般由客户端、配置服务器、配置仓库三部分组成,其中客户端是嵌入微服务的,配置服务器和配置仓库都是服务端,配置服务器主要是和客户端交互,配置仓库是存放配置信息,例如git、mysql等。
配置中心产品有很多,常见的有Spring Cloud Config、Disconf、Apollo、Consul、Diamond、Nacos、Etcd等。
配置中心主要的技术点在于客户端如何获取配置的变更,及如何配置变更通知机制,这一点可以类比注册中心。Nacos配置中心使用的是长轮询机制实现的。
(二)Nacos配置中心功能特性
1、配置中心三级隔离
在Nacos注册中心中,资源分为Namespace、Group、service三级,对于配置中心,同样分为三级,最后一级是配置中心的DataId,等同于注册中心的Service,所有关于注册中心的分级模型都适用于配置中心
2、DataId
DataId命名规则可以采用自定义的命名规则,推荐命名规则:${prefix}-${spring.profile.active}.${file-extension}
其中prefix默认为所属服务配置spring.application.name的值,也可以通过配置项 spring.cloud.nacos.config.prefix进行设置
spring.profiles.active为当前环境对应的Profile。注意:当 spring.profiles.active为空时,对应的连接符-也将不存在,DataId的拼接格式变成 ${prefix}.${file-extension}
file-exetension为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension来配置,推荐使用yaml格式
DataId配置文件格式目前只支持 yml 和 properties 两种,同时有bootstrap.yml和application.yml两类配置,bootstrap.yml是系统级别参数配置,一般不会变动,application.yml是用来定义应用级别的参数配置,在加载配置文件时,bootstrap.yml先加载,application.yml后加载。
配置文件的后缀必须与DataId的后缀保持一致,如:本地使用.yml,则Nacos中配置文件必须也应该是.yml
3、Nacos配置信息隔离
对于非生产环境,建议所有环境使用一个Nacos集群,在该Nacos集群中,基于某一个服务名称会自动搜索不同的配置项,所以在一个Nacos中可以根据Profile区分环境
对于生产环境,建议使用独立的Nacos,即服务配置和服务注册一样,完全物理隔离
4、Nacos配置共享
多个服务可能会有很多共用的配置,我们希望把这些共用配置抽取出来单独进行维护,避免各个服务重复创建和管理比如:数据库连接信息、Redis 连接信息、RabbitMQ连接信息、监控配置等。
如下图所示,有公共配置1、2,有服务A、B,服务A使用了共享配置1、2,那么服务A最终的配置就是共享配置1、共享配置2、服务A这三个配置的并集。
共享配置使用spring.cloud.nacos.config.shared-configs进行配置,这是一个数组,下面可以配置多个共享配置。
共享配置不能设置自定义的Group,只能为DEFAULT_GROUP。
spring:
application:
name: demo-service
cloud:
nacos:
config:
server-addr: local:8848
namespace: dev
group: demo_group
......
shared-configs[3]: # shared-configs数组
data-id: mysql.yaml # 不能设置自定义的Group, 只能为DEFAULT_GROUP
refresh: true # 默认为true
......
针对于共享配置不能设置自定义的Group的情况,如果想要自定义Group,就可以使用扩展配置。
扩展配置使用spring.cloud.nacos.config.extension-configs进行配置,也是一个数组,扩展配置必须指定特定的Group。如果要覆盖某个共享dataId 上的特定属性,可以使用 extension-configs数组。
spring:
application:
name: demo-service
cloud:
nacos:
config:
server-addr: local:8848
namespace: dev
group: demo_group
......
shared-configs[3]:
data-id: mysql.yaml
refresh: true
......
extension-configs[3]: # 如果要覆盖某个共享dataId 上的特定属性,可以使用 extension-configs数组
data-id: mysql.yaml
group: demo # 必须指定特定的Group
refresh: true
5、Nacos配置共享优先级
不同种类配置优先级:主配置 > 扩展配置(extension-configs) > 共享配置(shared-configs)
同种类配置优先级:数组元素对应的下标越大,优先级越高,如: extension-configs[2] > extension-configs[1] > extension-configs[0]
不同命名方式和位置:当前环境配置 > 不指定环境的配置 > 位于本地环境的配置
6、Nacos配置灰度发布:
配置的灰度发布就是让配置先在部分实例生效,如果效果理想全量发布到所有实例,如果效果不理想就可以放弃当前的发布内容。如:对于一些影响比较大的配置,可以先在一个或者多个实例生效,观察一段时间没问题后再全量发布配置;对于一些需要调优的配置参数,可以通过灰度发布功能来实现A/B测试。
在Nacos中提供了专门的灰度发布功能入口,即BETA版本,在BETA版本中填入发布地址,这个地址是灰度发布的目标服务器IP。灰度版本后,可以回滚,即删除该BETA,如果要升级,直接升级成正式即可。
7、Nacos配置热更新
热更新是在不重新启动服务的情况下,更细配置信息,就可以更新系统中运行的变量。
不是所有的场景都支持热更新,对于业务运行所需的数据,例如用户名密码、判断条件等,可以做热更新;而对于影响应用运行状态的配置来说,只能重启服务让其生效,不能通过热更新让其生效,例如数据库连接配置。
在使用Spring配置文件对属性进行赋值时,有两种操作方式,一种是使用@Value的方式,一种是使用@ConfigurationProperties的方式。
#自定义配置性
cs.customer.point=10
@Component
public class CustomConfig {
@Value("${cs.customer.point}") // @Value注解 一般做法是创建一个配置 类进行注入
private int point;
}
@Component
@ConfigurationProperties(prefix = "cs.customer") // @ConfigurationProperties注解 创建配置类,指定配置项的前缀 批量提取配置内容
public class CustomConfig {
private int point;
}
针对这两种获取配置信息方式,热更新是实现也不一样,对于使用@Value注解获取配置信息的方式,需要在该类上使用@RefreshScore注解,但是对于使用@ConfigurationProperties注解获取配置信息的方式,则不需要任何处理,因为@ConfigurationProperties注解 自动集成热更新机制,不需要代码控制,因此推荐使用。
#自定义配置性
cs.customer.point=10
@Component
@RefreshScope // @Value注解 在@Value注解的基础上添加@RefreshScope注解
public class CustomConfig {
@Value("${cs.customer.point}") // @Value注解一般做法是创建一个配置类进行注入
private int point;
}
@Component
@ConfigurationProperties(prefix = "cs.customer") // @ConfigurationProperties注解自动集成热更新机制,不需要代码控制,推荐使用
public class CustomConfig {
private int point;
}
(三)客服系统案例演进
引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
多环境配置
<profiles>
<profile>
<id>local</id>
<properties>
<spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
<spring.cloud.nacos.config.server-addr>192.168.249.130:8848</spring.cloud.nacos.config.server-addr>
<spring.profiles.active>local</spring.profiles.active>
<spring.cloud.nacos.discovery.namespace>dae2f8c4-a44a-4143-afc5-1f8aaa84c72c</spring.cloud.nacos.discovery.namespace>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>dev</id>
<properties>
<spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
<spring.cloud.nacos.config.server-addr>192.168.249.130:8848</spring.cloud.nacos.config.server-addr>
<spring.profiles.active>dev</spring.profiles.active>
<spring.cloud.nacos.discovery.namespace>b5b0791d-acb0-462e-9513-facb051a505f</spring.cloud.nacos.discovery.namespace>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
<spring.cloud.nacos.config.server-addr>192.168.249.130:8848</spring.cloud.nacos.config.server-addr>
<spring.profiles.active>test</spring.profiles.active>
<spring.cloud.nacos.discovery.namespace>734d2b15-9c71-4b61-bd52-67eec39e2774</spring.cloud.nacos.discovery.namespace>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
<spring.cloud.nacos.config.server-addr>192.168.249.130:8848</spring.cloud.nacos.config.server-addr>
<spring.profiles.active>prod</spring.profiles.active>
<spring.cloud.nacos.discovery.namespace>9b03f190-07c9-4fd7-bdc5-14f8f61bffb2</spring.cloud.nacos.discovery.namespace>
</properties>
</profile>
</profiles>
bootstrap.yml:
spring:
application:
name: middleground-customer-service
profiles:
active: local
cloud:
nacos:
config:
server-addr: @spring.cloud.nacos.config.server-addr@
file-extension: yml
refresh-enabled: true
namespace: @spring.cloud.nacos.discovery.namespace@
group: LCL_GALAXY_GROUP
discovery:
server-addr: @spring.cloud.nacos.discovery.server-addr@
namespace: @spring.cloud.nacos.discovery.namespace@
group: LCL_GALAXY_GROUP
cluster-name: beijing
nacos配置中心:
二、Nacos核心技术点解析
当需要分许一个框架原理时,主要的关注点是其设计思想、基本流程、通用机制、核心组件。
那么真对Nacos来说,其主要提供了注册中心和配置中心两个功能,对于注册中心来说,主要关注服务注册、服务发现和健康监测,对于配置中心来说,主要关注配置热更新。
(一)Nacos服务注册、发现和健康检测机制
注册中心基本模型如下所示,主要有服务提供者、服务消费者、注册中心,服务提供者向注册中心注册服务,服务消费者向注册中心订阅服务,如果服务发生变更,注册中心会通知服务消费者,而服务消费者在本地也有服务提供者的路由表缓存。
1、Nacos服务注册
(1)客户端流程
首先对于客户端来说,在启动时构建instance实例,然后调用NacosFactory工厂创建NamingService,然后调用NamingServer做服务注册,Namingservice调用代理类NamingClientProxy,代理类最终调用RpcClient向服务端发起注册请求。
NamingService在Nacos中非常重要,与其对应的是ConfigServer,即一个是注册中心的处理类,一个是配置中心的处理类。
下面是NamingService的源码,在注册方法registerInstance中,首先验证正确性、获取分组,然后如果当前实例是永久节点,则启动一个心跳,如果是临时节点则不启动心跳;最终调用serverProxy.registerService通过代理实现服务注册。
在取消注册方法deregisterInstance中,如果是永久节点,就移除心跳,临时节点不需要处理心跳,最终调用serverProxy.deregisterService通过代理取消服务注册。
public class NacosNamingService implements NamingService {
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) {
NamingUtils.checkInstanceIsLegal(instance);
// 验证正确性、获取分组
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
// 如果是永久节点,则启动心跳
if (instance.isEphemeral()) {
BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}
// 通过代理实现服务注册
serverProxy.registerService(groupedServiceName, groupName, instance);
}
@Override
public void deregisterInstance(String serviceName, String groupName, Instance instance) {
if (instance.isEphemeral()) {
beatReactor.removeBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), instance.getIp(), instance.getPort());
}
serverProxy.deregisterService(NamingUtils.getGroupedName(serviceName, groupName), instance);
}
}
在NamingProxy中,Nacos封装了一个HashMap来存储各类数据,例如 namespace、group等,然后执行reqAPI方法发起HTTP远程调用。
最终请求是通过Nacos封装的nacosRestTemplate的exchangeForm方法做的远程调用。
public class NamingProxy {
private final NacosRestTemplate nacosRestTemplate = NamingHttpClientManager.getInstance().getNacosRestTemplate();
public void registerService(String serviceName, String groupName, Instance instance) {
final Map<String, String> params = new HashMap<String, String>(16);
params.put(CommonParams.NAMESPACE_ID, namespaceId);
...
params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
// 封装请求,发起HTTP远程调用
reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
}
}
// 封装请求,发起HTTP远程调用
HttpRestResult<String> restResult = nacosRestTemplate.exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);
(2)服务端流程
对于服务端来说,首先是一个与客户端对应的RpcClient,然后执行register方法,最终进入Controller,然后调用ServiceManager进行注册。
在ServiceManager中,主要做了两件事,一个是将当前实例添加到服务列表中,然后在集群内同步本次注册。
下面是ServiceManager的源码,首先根据namespace、servicename、是否临时节点这三个属性生成一个唯一Key,然后从注册表中获取服务定义,将该实例添加到注册表中;最后调用consistencyService.put方法将本次变更同步给其它Nacos。
public class ServiceManager {
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) {
// 生成该服务实例的唯一键
String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
// 从注册表中获取服务定义
Service service = getService(namespaceId, serviceName);
synchronized (service) {
// 将服务实例添加到注册表中
List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
......
// 将本次变更同步给其它Nacos
consistencyService.put(key, instances);
}
}
}
ConsistencyService是一个接口,提供了put、remove、get等方法。
public interface ConsistencyService {
void put(String key, Record value) throws NacosException;
void remove(String key) throws NacosException;
Datum get(String key) throws NacosException;
void listen(String key, RecordListener listener) throws NacosException;
void unlisten(String key, RecordListener listener) throws NacosException;
boolean isAvailable();
}
ConsistencyService的类继承关系如下图所示,主要有三个实现类,分别是使用Raft算法的RaftConsistencyServiceImpl、使用Distro算法的DistroConsistencyServiceImpl、以及委托类DelegateConsistencyServiceImpl。
(3)服务注册设计亮点
服务注册的亮点或者流程主要就是服务存储和集群同步。
首先服务存储主要使用了双层Map结构,这也是注册中心主流存储方式,private Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
,第一层的key是Namespace,第二层的key是Group,第二层的value是Service;同时服务端还使用了缓存,用以提高服务实例获取性能。
集群同步机制采用的是通过事件在各个集群之间异步传递,做到了解耦,同时也方便扩展。
2、Nacos服务发现
(1)客户端流程
在Spring Cloud中服务发现使用的是DiscoveryClient,在Nacos中封装了一个NacosDiscoveryClient,其调用NacosNamingService来获取服务信息,而NacosNamingService则调用HostReactor获取服务信息。
HostReactor首先从缓存中获取信息,如果获取到则直接返回,去过获取不到,则调用代理类NamingProxy,最终使用RpcClient做远程调用获取服务信息,同时其还会启动一个定时任务定时从服务端获取最新的实例信息。
下面是HostReactor的源码,其首先读取本地服务列表的缓存:
如果没有数据,则创建一个空ServiceInfo,并立即更新服务列表serviceInfoMap、正在更新的服务列表updatingMap,然后调用updateServiceNow方法立即远程调用获取最新服务信息,待更新完成后,从正在更新的服务列表updatingMap中移除该服务。
如果缓存中有数据,但是正在更新的集合updatingMap中包含了该服务,即该服务正在被更新,则等待一段时间在返回。
然后执行scheduleUpdateIfAbsent方法启动一个定时任务从服务端获取最新的服务信息。
最后从服务列表缓存serviceInfoMap中获取服务信息。
简单的说,就是本地缓存有的话就直接取本地缓存,没有的话就远程调用获取服务信息,但是在远程调用前,先放一个空对象,然后在远程调用完成后将空对象替换掉,同时为了防止该服务正在更新但是又重新执行该方法,因此使用updatingMap来对正在更新的服务做了一个控制;最后启动了一个定时任务来定时获取最新的服务信息。
public class HostReactor {
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
String key = ServiceInfo.getKey(serviceName, clusters);
// 读取本地服务列表的缓存
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters);
// 缓存中没有则创建空ServiceInfo,并立即更新服务列表
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
// 缓存中有,但是需要更新
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
}
}
}
}
scheduleUpdateIfAbsent(serviceName, clusters);
// 返回缓存中的服务信息
return serviceInfoMap.get(serviceObj.getKey());
}
private void updateServiceNow(String serviceName, String clusters) {
try {
updateService(serviceName, clusters);
}
}
public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
}
// 不管是立即更新服务列表,还是定时更新服务列表,最终都是调用updateService方法
public void updateService(String serviceName, String clusters) throws NacosException {
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
// 基于ServerProxy发起远程调用,查询服务列表
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);
if (StringUtils.isNotEmpty(result)) {
processServiceJson(result);
}
}
}
}
(2)服务端流程
服务端使用与客户端对称的RpcClient接收请求,然后调用Controller、InstanceOperatorServiceImpl来获取服务信息。
同时在InstanceOperatorServiceImpl中还执行addClient方法将客户端维护起来,维护客户端的主要作用是如果后续有服务发生变化,服务端可以通过PushClient将变化推送到客户端。
服务端推送类PushService的源码如下所示,其使用了时间监听机制,当发生了服务变更事件后,会定时使用UDP的方式将服务信息推送到客户端。
public class PushService {
// 监听服务变更事件
public void onApplicationEvent(ServiceChangeEvent event) {
// 从事件中获取服务相关信息
Service service = event.getService();
// 定时发送UDP推送
Future future = GlobalExecutor.scheduleUdpSender(() -> {
try {
for (PushClient client : clients.values()) {
Receiver.AckEntry ackEntry = prepareAckEntry(client, compressData, data, lastRefTime);
// 执行UDP推送
udpPush(ackEntry);
}
}
}, 1000, TimeUnit.MILLISECONDS);
...
}
}
(3)服务发现设计亮点
从客户端来说,其主要使用了客户端缓存、定时任务、和服务订阅。
客户端缓存:优先从本地缓存Map<String, ServiceInfo> serviceInfoMap
中获取服务实例信息,可以提高性能
定时任务:维护定时任务从服务端获取服务实例信息,并同步本地缓存
服务订阅:具备服务订阅机制,可以获取来自服务端的异步推送
从服务端来说,主要是异步推送,即开启一个UDP推送服务,将服务实例变更信息推送给客户端
3、健康检测
(1)健康检测的基本思路:
客户端主动上报机制:客户端主动上报,告诉服务端自己的健康状态,如果一段时间没有上报则就认为服务已不健康
服务端主动探测机制:服务端主动对客户端发起探测,看看客户端是否有响应,如果没有则认为该服务已不健康
(2)Nacos健康检查机制:
临时实例:临时存在于注册中心中,会与注册中心保持心跳,注册中心会在一段时间没有收到来自客户端的心跳后将实例设置为不健康,然后在一段时间后进行剔除
永久实例:会永久的存在于注册中心,且有可能并不知道注册中心存在,不会主动向注册中心上报心跳
(3)Nacos健康检测 - 临时实例:
临时实例心跳:
Nacos客户端会维护一个定时任务,每隔5秒发送一次心跳请求
Nacos服务端收到心跳请求后会刷新心跳时间,如果在15秒内如果没有收到客户端的心跳请求,会将该实例设置为不健康,并通过PushService发送服务变更事件。
Nacos服务端在30秒内没有收到心跳,会将这个临时实例摘除
也可以通过配置修改上述的值:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
heart-beat-interval: 5000
heart-beat-timeout: 15000
ip-delete-timeout: 30000
(4)Nacos健康检测 - 永久实例
永久实例探测:
采用服务端主动健康检测方式,探测周期为2000 + 5000毫秒内的随机数,使用随机值主要是为了防止在同一时间点调用对服务造成的压力。
检测异常后只会标记为不健康,不会删除。
注册中心主动探测可以使用TCP探测、HTTP探测、MYSQL探测,其中在微服务中只会使用TCP探测和HTTP探测。
(二)Nacos配置热更新机制
1、基本思路:
推(Push)模式:服务端主动将数据变更信息推送给客户端时效性好,但复杂度高服务端和客户端需要更多资源来维持连接
拉(Pull)模式:客户端主动去服务器请求数据实现简单,但时效性差重复请求会导致服务器压力过大
2、长轮询机制:
Nacos采用长轮询机制来实现热更新。
首先客户端发送一个HTTP请求,同时设置超时时间,服务端如果在特定时间内发生了变化,会立即返回客户端请求,这就类似于推模式;如果服务端没有发生变化,客户端会发生超时,这就类似于拉模式;同时客户端在收到服务端响应后会立即重新发起新的连接。
使用长轮询机制避免了推模式和拉模式的缺点,同时也汇集了推模式和拉模式的优点,既做到了低延时,又做到了轻资源。
低延时:客户端发起长轮询,服务端感知到数据发生变更后,能立刻返回响应给客户端
轻资源:客户端发起长轮询,如果数据没有发生变更,服务端会hold住此次客户端的请求,不会消耗太多服务端资源
3、Nacos长轮询
在Nacos中,客户端不会频繁发起轮询,也不需要维持与客户端的心跳,而是发起一个获取服务端配置的请求,服务端在收到请求后,将请求封装成ClientLongPolling并放入allSubs集合,ClientLongPolling的执行时间是29.5秒,等待29.5秒后,将结果返回,如果在29.5秒内监听到LocalDataEvent事件,即配置变更事件,则循环allSubs找到对应的ClientLongPolling并返回。
这样做服务端兼顾时效性和复杂度。
4、客户端核心组件
客户端的核心组件主要有NacosConfigService、ClientWorker、LongPollingRunnable、HttpAgent。
NacosConfigService:配置服务,封装客户端操作
ClientWorker:客户端任务,启动和管理定时器线程
LongPollingRunnable:长轮询线程,触发服务器轮询操作
HttpAgent:执行HTTP远程调用,设置超时时间为30秒
下图是客户端的流程,在启动时会初始化NacosConfigService,然后初始化ClientWorker。
在ClientWorker中会初始化一个每10秒执行一次的线程池来执行长轮询LongPollingRunnable。
public class ClientWorker implements Closeable {
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
// 每10ms执行一次
this.executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
checkConfigInfo();
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
public void checkConfigInfo() {
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// 执行长轮询线程
executorService.execute(new LongPollingRunnable(i));
}
...
}
}
}
在LongPollingRunnable中,首先检查本地配置文件,然后发起服务器数据变更监听操作并设置长连接超时时间,再从服务器获取最新配置信息,最后重复该操作。
class LongPollingRunnable implements Runnable {
@Override
public void run() {
// 检查本地配置文件
checkLocalConfig(cacheData);
// 发起服务器数据变更监听操作, 设置长连接超时时间
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
// 从服务器获取最新配置信息
ConfigResponse response = getServerConfig(dataId, group, tenant, 3000L);
// 客户端重复上述步骤
executorService.execute(this);
}
}
5、服务端核心组件
服务端核心组件有ConfigController、LongPollingService、ClientLongPolling、LocalDataChangeEvent。
ConfigController:接收来自客户端的长轮询
LongPollingService:根据目标DataId+Group,启动异步线程执行轮询操作
ClientLongPolling:长轮询线程,维护请求和响应的对应关系
LocalDataChangeEvent:配置变更事件,被LongPollingService监听和处理
下图是服务端执行流程图,首先服务端接收来自客户端的长轮询请求,然后调用长轮询服务LongPollingService启动轮询,在LongPollingService中会监听配置变更事件LocalDataChangeEvent,同时设置超时时间,然后启动线程池执行ClientLongPolling,如果在超时时间内没有监听事件发生,则返回客户端,如果在超时时间内发生了监听事件,则DataChangeTask通过ClientLongPolling发送结果。
(1)AsyncContext
在看源码之前,先看一下AsyncContext,这是servlet3提供的异步上下文,在Servlet中有doGet方法,在该方法中,根据请求获取AsyncContext,然后异步执行长轮询,在处理中可以通过asyncContext.getResponse().getWriter().write回写处理结果,在处理完毕后调用asyncContext.complete()通知Servlet容器。
public class SimpleAsyncHelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取该请求对应的AsyncContext
AsyncContext asyncContext = request.startAsync();
// 执行异步处理
asyncContext.start(() -> {
new LongRunningProcess().run();
try {
asyncContext.getResponse().getWriter().write("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
// 执行完毕通知Servlet容器
asyncContext.complete();
});
}
}
(2)LongPollingService
在Nacos的长轮询机制中就是采用的AsyncContext,代码如下所示,在addLongPollingClient中,接收到客户端请求后,首先计算长轮询超时时间,避免客户端超时,然后计算配置数据是否发生变更,这里的变更通过MD5算法进行判断;然后启动一个定时任务来判断配置是否发生了变更,该定时任务有超期时间,如果在指定时间内没有发生变更,就直接返回。
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) {
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
// 计算长轮询超时时间,避免客户端超时
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
if (isFixedPolling()) {
} else {
// 计算配置数据变更
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
...
}
// 使用AsyncContext开启异步线程
final AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0L);
scheduler.execute(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}
(3)配置变更事件
在addLongPollingClient中有事件监听,监听LocalDataChangeEvent,如果配置发生变化,则启动异步任务执行DataChangeTask,在DataChangeTask中主要是构建响应结果并异步返回,其最终执行的是generateResponse方法,在该方法中调用了asyncContext.complete()做异步返回。
public void onEvent(Event event) {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
// 监听事件,启动异步任务
scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
// 监听事件,启动异步任务
class DataChangeTask implements Runnable {
@Override
public void run() {
try {
ConfigService.getContentBetaMd5(groupKey);
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
...
// 构建响应结果并异步返回
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
}
}
}
void generateResponse(List<String> changedGroups) {
HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();
response.getWriter().println(respString);
// 构建响应结果并异步返回
asyncContext.complete();
}
三、使用Spring Cloud Stream重构消息通信机制
(一)Spring Cloud Stream整体架构
在系统集成场景中,提到了企业集成模式的解决方案,包括通道、路由器、转换器等,这种解决方案的背后,实际上也是一种通道跟消息的转换,即各个端点(Endpoint)通过发送消息的方式把消息传递到应用系统中,然后通过通道、路由器、转换器,最终到达目标系统。
我们可以将总线涉及到的数据结构抽象成一个消息,或者转换成中间件中的一个通道就可以了,本质上,其应该是有消息加通道的整合,从而形成一个跨平台的系统集成模式。
现在我们使用的是RocketMQ,如果换一种消息中间件呢,例如Kafka、RabbitMQ,那就需要换API、升级版本、客户端要重新适配等问题,对系统的开发成本和扩展性有比较大的影响。
为了解决不同的消息中间件的差异,就要使用平台化消息通信机制,即屏蔽各种中间件在使用上的差别,从而为开发人员提供了一套统一且高效的消息发送和接收API。
如下图所示,不同的服务都对接统一的API,然后统一的API再去使用不同的消息中间件。
Spring对于消息通信提供了一整套的解决方案,分别有 Spring Messaging、Spring Integration、Spring Cloud Stream。
最底层的消息通信抽象是Spring Messaging,其脱离于任何的框架,可以将其理解为一个框架,Spring容器自带的,在Spring Core中。
在Spring Messaging之上又封装了一层Spring Integration,其是一个独立的框架,主要是在Spring Messaging提供的基础通信能力外,提供了面向系统集成方面的能力,这种能力有点类似上面的总线架构。目前Spring Integration存在感不是很强,用的不是很多,但是在ESB架构中,还是会有一些应用。
最上层的是Sring Cloud Stream,他可以将RocketMQ、Kafka、RabbitMQ等整合成一套,使用起来像是用了同一个中间件。
Spring Cloud Stream是一个非常对称的架构,其就是在消息生产者和消费者之间嫁接了一个桥梁,中间是消息中间件,这个中间件可以是任何一种,例如RocketMQ、Kafka、RabbitMQ等,但是服务A和服务B都没有直接和消息中间件对接,有的只是间接和其交互的过程。
在消费生产者和消费者都是直接和Spring Cloud Stream直接交互,但是内部组件又不太一样。在生产者侧有Source、Channel、Binder,在消费侧有Sink、Channel、Binder,两边只有Source和Sink不一样,Source意味着源头,Sink意味着下沉,对应到中间件的术语来说,Source就是消息发布者、Sink就是消息消费者,Channel和Binder两边都是对称的,因此Spring Cloud Stream是一个非常对称的架构。
Spring Cloud Stream核心组件:
(1)Source + Sink
Source和Sink是两个接口,在Source中,有MessageChannel,在Sink中有SubscribableChannel,这两个都是Spring Messaging中的通道,通过名字就可以看出,一个是发布者,一个是订阅者,同时还用了@Output和@Input注解来标识出和入。那么我们注入Source组件就可以发送消息,注入Sink组件就可以消费消息。
// 真正生成消息的组件
public interface Source {
String OUTPUT = "output";
// 来自Spring Messaging
@Output(Source.OUTPUT)
MessageChannel output();
}
// 真正消费消息的组件
public interface Sink {
String INPUT = "input";
// 来自Spring Messaging
@Input(Source.INPUT)
SubscribableChannel input();
}
(2)Channel
可以简单把通道理解为是对队列的一种抽象,通道的名称和队列相同,但是作为一种抽象和封装,各个消息通信系统所特有的队列概念并不会直接暴露在业务代码中,而是通过通道来对队列进行配置。简单地说,对于业务侧,叫做通道,是一个逻辑概念,对于消息中间件来说,叫做队列,是一个物理概念。
同时也可以自定义Channel,如下代码所示,定义了一个输入通道和两个输出通道
// 定义一个输入通道和两个输出通道
public interface MyChannel {
@Input
SubscribableChannel input1();
@Output
MessageChannel output1();
@Output
MessageChannel output2();
}
(3)Binder
所谓Binder,顾名思义就是一种黏合剂,将业务服务与消息通信系统黏合在一起。通过Binder,我 们可以很方便的连接消息中间件,可以动态的改变消息的目标地址、发送方式,而不需要了解其背后的各种消息中间件在实现上的差异,从而显著提高了广大开发人员的开发效率。
Spring Cloud Stream目前集成了RabbitMQ、Kafka、RocketMQ三个消息中间件,对应的Binder分别是 RabbitMQ Binder、Kafka Binder、RocketMQBinder。
(二)Spring Cloud Stream应用方式
1、发送消息
通过StreamBridge的send方法发送消息,send方法参数是一个Output和一个消息体,这个消息体可以是普通的POJO,也可以是封装的SpringMessage对象。
@Component
public class StreamProducer {
// 通过StreamBridge发送消息
@Autowired
private StreamBridge streamBridge;
public static String CLUSTER_MESSAGE_OUTPUT = "cluster-out-0";
public void sendEvent(Event event) {
Message<Event> message = new GenericMessage<>(event);
streamBridge.send(CLUSTER_MESSAGE_OUTPUT, message);
}
}
StreamBridge的send方法源码如下代码所示,首先会判断message是否是Message对象,如果是,直接做后续处理,如果不是,则先将其封装成Message对象。
然后根据配置信息获取一个messageChannel通道,再调用Channel的send方法发送消息。
public final class StreamBridge implements SmartInitializingSingleton {
public boolean send(String bindingName, @Nullable String binderName, Object data, MimeType outputContentType) {
if (!(data instanceof Message)) {
data = MessageBuilder.withPayload(data).build();
}
MessageChannel messageChannel = this.resolveDestination(bindingName, producerProperties, binderName);
if (data instanceof Message) {
data = MessageBuilder.fromMessage((Message) data).setHeader(MessageUtils.TARGET_PROTOCOL, "streamBridge").build();
}
Message<byte[]> resultMessage = (Message) ((Function) functionToInvoke).apply(data);
// 底层还是通过MessageChannel 发送消息
return messageChannel.send(resultMessage);
}
}
上面提到需要根据配置信息获取channel,下面的配置是使用RocketMQ的配置,首先配置了使用rocketmq,然后配置其NameServer、Source、生产者组、topic,其中Source代表消息生产者,针对于生产者再配置生产者组;另外topic是在bindings下的source名称下的destination,这是因为不同的消息队列有不同的叫法,不一定都有topic,因此使用了destination表示,在RocketMQ的场景,对应的就是Topic。
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876 # 指定RocketMQ NameServer
bindings:
cluster-out-0: # Source
producer:
group: output_0_group # 指定生产者组
bindings:
cluster-out-0:
destination: cluster # 指定Topic
2、消费消息
消费消息直接使用函数式编程的方式就可以消费消息,老版本需要使用@Sreamlistener注解,新版本已经不需要了;但是有一点需要注意,消费者的方法需要与配置一致。
@Component
public class StreamConsumer {
@Bean
public Consumer<Event> consume() {
// 函数式编程方式响应消息
return message -> {
System.out.println("Received message : " + message);
...
};
}
}
消费者的配置如下代码所示,首先是配置消费者的函数式方法function.definition,这里的配置要和上面的函数式方法明保持一致,另外还需要配置NameSeerver、Sink,Sink中要配置消费的topic和消费者组。
spring:
cloud:
stream:
function:
definition: cluster
rocketmq:
binder:
name-server: localhost:9876 # 指定RocketMQ NameServer
bindings:
cluster-in-0: # Sink
destination: cluster # 指定Topic
group: cluster-group # 指定消息分组
3、延迟消息
对于某些特定的场景,需要有特殊的设置,例如RocketMQ的延迟队列,就需要在消息头中设置Delay Level,这里的Delay Level是RocketMQ中的MessageConst.PROPERTY_DELAY_TIME_LEVEL,由于延迟队列是RocketMQ中的特有功能,Spring Cloud Stream牺牲了一些通用性,需要我们单独进行设置。
@Component
public class StreamDelayProducer {
@Autowired
private StreamBridge streamBridge;
public static String CLUSTER_MESSAGE_OUTPUT = "cluster-out-0";
public void sendEvent(Event event) {
Map<String, Object> headers = new HashMap<>();
// 通过消息头指定Delay Level
headers.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 4);
Message<Event> message = new GenericMessage<>(event, headers);
streamBridge.send(CLUSTER_MESSAGE_OUTPUT, message);
}
}
(三)客服系统案例演进
在系统中使用Spring Cloud Stream替换RocketMQ
1、生产者
引入spring-cloud-starter-stream-rocketmq组件
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
配置信息:
这里要注意,customer-staff-change-stream-out是Source的名称,可以根据业务进行变更,但是后续绑定的要一致。
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876 # 指定RocketMQ NameServer
bindings:
customer-staff-change-stream-out: # Source
producer:
group: producer_customstaff_changed_stream # 指定生产者组
bindings:
customer-staff-change-stream-out:
destination: topic_staff_stream # 指定Topic
通过StreamBridge发送消息
@Component
public class CustomerStaffChangedEventStreamProducer {
@Autowired
private StreamBridge streamBridge;
private static String CLUSTER_MESSAGE_OUTPUT = "customer-staff-change-stream-out";
public void sendCustomerStaffChangedEvent(CustomerStaff customerStaff, String operation){
CustomerStaffEventDTO customerStaffEventDTO = new CustomerStaffEventDTO();
customerStaffEventDTO.setId(customerStaff.getId());
customerStaffEventDTO.setStaffName(customerStaff.getStaffName());
customerStaffEventDTO.setAccountId(customerStaff.getAccountId());
customerStaffEventDTO.setPhone(customerStaff.getPhone());
CustomerStaffChangedEvent event = new CustomerStaffChangedEvent();
event.setType("STAFF");
event.setOperation(operation);
event.setMessage(customerStaffEventDTO);
Message<CustomerStaffChangedEvent> message = new GenericMessage<>(event);
streamBridge.send(CLUSTER_MESSAGE_OUTPUT, message);
}
}
2、消费者
同样引入spring-cloud-starter-stream-rocketmq组件。
配置信息:
这里要注意,definition的值要和后续消费者的方法名一致,同时使用*-in-0配置Sink时,星号要使用definition替换。
spring:
cloud:
stream:
function:
definition: staffChangedEventConsumer
rocketmq:
binder:
name-server: localhost:9876 # 指定RocketMQ NameServer
bindings:
staffChangedEventConsumer-in-0: # Sink
destination: topic_staff_stream # 指定Topic
group: consumer_group_staff_changed_stream # 指定消息分组
使用函数式编程方式响应消息:方法名称要和definition名称一致。
@Component
public class CustomerStaffTagStreamConsumer {
@Autowired
private ILocalCustomerStaffService localCustomerStaffService;
@Bean
public Consumer<CustomerStaffChangedEvent> staffChangedEventConsumer(){
return message ->{
System.out.println("Received message : " + message);
CustomerStaffEventDTO dto = message.getMessage();
LocalCustomerStaff localCustomerStaff = new LocalCustomerStaff();
convertLocalCustomerStaff(dto, localCustomerStaff);
String operation = message.getOperation();
if(operation.equals("CREATE")) {
localCustomerStaffService.insertLocalCustomerStaff(localCustomerStaff);
} else if(operation.equals("UPDATE")) {
localCustomerStaffService.updateLocalCustomerStaff(localCustomerStaff);
} else if(operation.equals("DELETE")) {
localCustomerStaffService.deleteLocalCustomerStaff(localCustomerStaff);
}
};
}
private void convertLocalCustomerStaff(CustomerStaffEventDTO dto, LocalCustomerStaff localCustomerStaff) {
localCustomerStaff.setStaffId(dto.getId());
localCustomerStaff.setStaffName(dto.getStaffName());
localCustomerStaff.setAccountId(dto.getAccountId());
localCustomerStaff.setPhone(dto.getPhone());
}
}
四、Spring Cloud Stream和消息中间件整合机制解析
(一)Spring Cloud Stream Binder
上面提到 Spring Cloud Stream 真正发送消息的是 Source 接口,真正消费消息的是 Sink 接口。
在Source中,提供了output方法,该方法返回了MessageChannel;在Sink中,提供了input方法,该方法返回值是SubscribableChannel。
// 真正生成消息的组件
public interface Source {
String OUTPUT = "output";
// 来自Spring Messaging
@Output(Source.OUTPUT)
MessageChannel output();
}
// 真正消费消息的组件
public interface Sink {
String INPUT = "input";
// 来自Spring Messaging
@Input(Source.INPUT)
SubscribableChannel input();
}
但是在上面的案例应用中并没有使用Source和Sink,那么源码阅读就需要首先搞清楚这两个接口在哪个节点起作用,然后找到它的访问入口进行切入就可以了。通过代码的反向引用来找到这两个注解所使用的核心入口,即BindableProxyFactory。
BindableProxyFactory是一个工厂了,其继承了AbstractBindableProxyFactory,并实现了MethodInterceptor、FactoryBean、InitializingBean、BeanFactoryAware四个接口。
AbstractBindableProxyFactory实现类Bindable接口,来实现具体的绑定操作;MethodInterceptor扩展了Interceptor接口,实现访问拦截;FactoryBean是Spring中的工厂Bean,用来获取Bean;InitializingBean是Spring中初始化Bean接口;BeanFactoryAware是Spring中获取容器对象的接口。
因此BindableProxyFactory是一个代理工厂类,也就意味着他肯定是个拦截器,同时也可以生成一些对象。
public class BindableProxyFactory
extends AbstractBindableProxyFactory // 实现Bindable接口,执行具体绑定操作
implements MethodInterceptor, // 扩展Interceptor接口,实现访问拦截
FactoryBean<Object>, // 获取Bean
InitializingBean, // 初始化Bean
BeanFactoryAware { // 获取容器对象
}
1、初始化
(1)BindingTargetFactory -InitializingBean
首先看BindingTargetFactory实现的InitializingBean接口,InitializingBean扩展点就是在容器初始化后执行afterPropertiesSet方法。
在BindingTargetFactory的afterPropertiesSet方法中,其使用反射工具类ReflectionUtils和注解工具类AnnotationUtils来获取Output注解,对应的就是Source组件。
如果Output注解不为空,则使用BoundTargetHolder封装@Output注解返回类型,并将其存储起来,而本质上存储的是MessageChannel。
public class BindableProxyFactory extends AbstractBindableProxyFactory implements MethodInterceptor, FactoryBean<Object>, InitializingBean, BeanFactoryAware {
public void afterPropertiesSet() {
this.populateBindingTargetFactories(this.beanFactory);
ReflectionUtils.doWithMethods(this.type, (method) -> {
// 获取@Output注解
Output output = (Output) AnnotationUtils.findAnnotation(method, Output.class);
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils.getBindingTargetName(output, method);
Class<?> returnType = method.getReturnType();
// 存储@Output注解返回类型,本质上是MessageChannel
this.outputHolders.put(name, new BoundTargetHolder(this.getBindingTargetFactory(returnType).createOutput(name), true));
}
});
//省略绑定Input注解代码
}
}
(2)BindingTargetFactory -获取MessageChannel
上面提到最终使用BoundTargetHolder封装@Output注解返回类型,并将其存储起来,而本质上存储的是MessageChannel,那么获取MessageChannel是调用的BindingTargetFactory类中的BindingTargetFactory接口的createOutput方法。
BindingTargetFactory的实现类是SubscribableChannelBindingTargetFactory,在其createOutput方法中,根据name从容器中获取Channel,如果获取则返回,如果容器中没有,则新创建一个返回。
public class SubscribableChannelBindingTargetFactory extends AbstractBindingTargetFactory<SubscribableChannel> {
public SubscribableChannel createOutput(String name) {
SubscribableChannel subscribableChannel = null;
if (this.context != null && this.context.containsBean(name)) {
try {
// 直接从容器中获取MessageChannel
subscribableChannel = (SubscribableChannel) this.context.getBean(name, SubscribableChannel.class);
}
} if (subscribableChannel == null) {
// 创建一个DirectChannel
DirectWithAttributesChannel channel = new DirectWithAttributesChannel();
...
subscribableChannel = channel;
} return (SubscribableChannel) subscribableChannel;
}
}
2、MethodInterceptor - 访问拦截
MethodInterceptor集成了Interceptor接口,就是一个拦截器,BindableProxyFactory实现了MethodInterceptor,那么在执行时就会被拦截并执行invoke方法,首先从缓存中获取MessageChannel,如果有则直接返回,如果没有,就从上面封装的BoundTargetHolder中获取。
// AOP中的拦截器
public interface MethodInterceptor extends Interceptor {
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
public class BindableProxyFactory extends AbstractBindableProxyFactory implements MethodInterceptor ... {
public synchronized Object invoke(MethodInvocation invocation) {
Method method = invocation.getMethod();
// 获取MessageChannel,并添加缓存机制
Object boundTarget = this.targetCache.get(method);
if (boundTarget != null) {
return boundTarget;
} else {
Output output = (Output) AnnotationUtils.findAnnotation(method, Output.class);
if (output != null) {
String name = BindingBeanDefinitionRegistryUtils.getBindingTargetName(output, method);
// 获取MessageChannel,并添加缓存机制
boundTarget = ((BoundTargetHolder) this.outputHolders.get(name)).getBoundTarget();
this.targetCache.put(method, boundTarget);
return boundTarget;
}
...
}
}
}
4、Bindable
(1)Bindable - 定义
BindableProxyFactory继承了AbstractBindableProxyFactory,而AbstractBindableProxyFactory实现了Bindable接口,在该接口中,提供了一组绑定、解绑操作方法。
public interface Bindable {
default Collection<Binding<Object>> createAndBindInputs(BindingService adapter) {
return Collections.emptyList();
}
default Collection<Binding<Object>> createAndBindOutputs(BindingService adapter) {
return Collections.emptyList();
}
// 针对Input和Output的绑定和解绑操作
default void unbindInputs(BindingService adapter) {
}
// 针对Input和Output的绑定和解绑操作
default void unbindOutputs(BindingService adapter) {
}
default Set<String> getInputs() {
return Collections.emptySet();
}
default Set<String> getOutputs() {
return Collections.emptySet();
}
}
(2)Bindable - 实现
上面提到AbstractBindableProxyFactory实现了Bindable接口。
以绑定消息发送者方法createAndBindInputs为例,首先从inputHolders中获取boundTargetHolderEntry,如果boundTargetHolderEntry的value为空,即还没有绑定,则通过BindingService实现绑定。
public class AbstractBindableProxyFactory implements Bindable {
public Collection<Binding<Object>> createAndBindInputs(BindingService bindingService) {
List<Binding<Object>> bindings = new ArrayList();
Iterator iterator = this.inputHolders.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, BoundTargetHolder> boundTargetHolderEntry = (Entry) var3.next();
BoundTargetHolder boundTargetHolder = (BoundTargetHolder) boundTargetHolderEntry.getValue();
String outputTargetName = (String) boundTargetHolderEntry.getKey();
if (((BoundTargetHolder) boundTargetHolderEntry.getValue()).isBindable()) {
// 通过BindingService实现绑定
bindings.add(bindingService.bindProducer(boundTargetHolder.getBoundTarget(), outputTargetName));
}
}
return bindings;
}
}
(3)BindingService
在BindingService的bindProducer方法中,首先获取一个Binder,然后调用doBindProducer执行绑定操作。
public class BindingService {
public <T> Binding<T> bindProducer(T output, String outputName, boolean cache, @Nullable Binder<T, ?, ProducerProperties> binder) {
String bindingTarget = this.bindingServiceProperties.getBindingDestination(outputName);
if (binder == null) {
// 获取Binder
binder = this.getBinder(outputName, outputClass);
}
// 执行绑定操作
Binding<T> binding = this.doBindProducer(output, bindingTarget, binder, (ProducerProperties) producerProperties);
if (cache) {
this.producerBindings.put(outputName, binding);
}
return binding;
}
}
(4)Binder - 定义和获取过程
上面提到的获取Binder是根据Channel名称获取的,首先是通过配置信息获取binderConfigurationName,然后使用binderFactory来获取。
binderFactory接口提供了getBinder方法进行获取Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties>。
而真正实现绑定的是Binder接口,Binder接口提供了绑定消费者的方法bindConsumer,以及绑定生产者的方法bindProducer。
public class BindingService {
protected <T> Binder<T, ?, ?> getBinder(String channelName, Class<T> bindableType) {
// 根据Channel名称获取Binder
String binderConfigurationName = this.bindingServiceProperties.getBinder(channelName);
return this.binderFactory.getBinder(binderConfigurationName, bindableType);
}
}
public interface BinderFactory {
<T> Binder<T, ? extends ConsumerProperties, ? extends ProducerProperties> getBinder(String configurationName, Class<? extends T> bindableType);
}
public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> {
Binding<T> bindConsumer(String name, String group, T inboundBindTarget, C consumerProperties);
Binding<T> bindProducer(String name, T outboundBindTarget, P producerProperties);
}
5、Binder - 类层结构
Binder类结构如下所示,有AbstractBinder和AbstractMessageChannelBinder两个实现类,然后对应不同消息中间件的实现类。
(1)AbstractMessageChannelBinder--顶层抽象
AbstractMessageChannelBinder提供了用于完成消息的发送的方法createProducerMessageHandler,返回对象是MessageHandler,而MessageHandler是一个处理消息的过程。
该方法的作用是完成消息发送,处理流程是把中间件对应的消息数据结构型转换成Spring Messaging中统一的Message消息对象。
AbstractMessageChannelBinder 还提供了用于完成消息的消费的createConsumerEndpoint方法,返回对象是MessageProducer,而MessageProducer是一个产生消息的过程。
该方法的作用是完成消息的消费,处理流程是把Spring Messaging中统一的Message消息对象转化为中间件对应的消息数据结构。
这两个方法是一个顶层抽象,一正一反,做了对象的转换,把整个流程串了起来。这两个抽象方法的实现是需要不同的消息中间件来实现的。
public abstract class AbstractMessageChannelBinder {
// 用于完成消息的发送 把中间件对应的消息数据结构型转换成Spring Messaging中统一的Message消息对象
protected abstract MessageHandler createProducerMessageHandler(ProducerDestination destination, P producerProperties, MessageChannel errorChannel) throws Exception;
// 用于完成消息的消费 把Spring Messaging中统一的Message消息对象转化为中间件对应的消息数据结构型成
protected abstract MessageProducer createConsumerEndpoint(ConsumerDestination destination, String group, C properties) throws Exception;
}
(2)AbstractMessageChannelBinder--抽象应用
在AbstractMessageChannelBinder中有doBindProducer方法,其调用了子类(不同中间件自己的实现)的createProducerMessageHandler方法获取一个MessageHandler,然后使用MessageHandler来创建一个SendingHandler,这样发送消息就可以通过SendingHandler进行处理,而 SendingHandler通过MessageHandler实现底层消息处理。
也就是说发送消息是通过SendingHandler来的,但是底层是通过MessageHandler来处理的。
同时在AbstractMessageChannelBinder中有doBindConsumer方法,其调用子类(不同中间件自己的实现)的createConsumerEndpoint方法来获取一个类型为MessageProducer的consumerEndpoint,但是他也不是真正消费消息,而是设置了一个OutputChannel通道,这个通道被StreamListenerMessageHandler所监听,这也对应了老版本监听者的@StreamListener注解,而新版去掉了这个注解,使用了函数式编程来实现的,但是本质上底层还是使用了StreamListenerMessageHandler来进行消息消费。
也就是说consumerEndpoint只是用来获取消息,但是底层使用StreamListenerMessageHandler来处理的。
根据以上的分析,其是将消息接收和消息处理进行了解耦。
public final Binding<MessageChannel> doBindProducer(final String destination, MessageChannel outputChannel, final P producerProperties) throws BinderException {
......
ProducerDestination producerDestinatio = ...;
MessageHandler producerMessageHandler = createProducerMessageHandler(producerDestination, producerProperties, errorChannel);
......
// 发送的消息通过SendingHandler进行处理,而 SendingHandler通过MessageHandler实现底层消息处理
((SubscribableChannel) outputChannel).subscribe(new SendingHandler(producerMessageHandler...));
Binding<MessageChannel> binding = new DefaultBinding<MessageChannel>(destination, null, outputChannel, producerMessageHandler...){
......
}
return binding;
}
public final Binding<MessageChannel> doBindConsumer(String name, String group, MessageChannel inputChannel, final C properties) throws BinderException {
MessageProducer consumerEndpoint = null;
try {
final ConsumerDestination destination = ...;
// 接收的消息通过ConsumerEndpoint进行 处理,类型为MessageProducer
consumerEndpoint = this.createConsumerEndpoint(destination, group, properties);
// 订阅inputChannel消息通道的是 StreamListenerMessageHandler
consumerEndpoint.setOutputChannel(inputChannel);
this.consumerCustomizer.configure(consumerEndpoint, name, group);
Binding<MessageChannel> binding = new DefaultBinding<MessageChannel>(name, group, inputChannel, consumerEndpoint...){
...
} ;
return binding;
}
}
6、消息发送和消费全流程
根据上面的分析,Spring Cloud Stream消息发送和消费的整体流程和核心组件如下所示。
首先最核心的就是消息中间件以及MessageHandler和MessageProducer。
对于发送消息的流程,使用output的MessageChannel进行发送消息,而SendingHandler订阅了output的channel,一旦该channel中有消息,就会被SendingHandler感知,然后SendingHandler接收消息后,使用MessageHandler将消息发送的消息中间件。
对于消费消息的流程,使用MessageProducer从消息中间件获取数据,然后发送消息到input的MessageChannel,然后StreamListenerMessageHandler订阅了input的MessageChannel,一旦input的MessageChannel中有消息,就会被StreamListenerMessageHandler监听到并处理。
(二)Spring Cloud Stream集成消息中间件
Spring Cloud Stream中主流的Binder有RabbitMQ Binder、Kafka Binder、RocketMQBinder,但是也有一些小众的消息中间件和一些云厂家所提供的的一些消息中间件。
下面主要分析RocketMQ和RabbitMQ,来体会一下平台化API的好处。
1、RocketMQ集成:
(1)消息发布
RocketMQ提供了RocketMQMessageChannelBinder,其重写了createProducerMessageHandler方法。
在该方法中首先获取配置信息,然后创建了 RocketMQProducerMessageHandler,并设置了相关配置信息。
public class RocketMQMessageChannelBinder {
protected MessageHandler createProducerMessageHandler(...) throws Exception {
RocketMQProducerProperties mqProducerProperties = (RocketMQProducerProperties) RocketMQUtils.mergeRocketMQProperties(...);
// 创建RocketMQProducerMessageHandler
RocketMQProducerMessageHandler messageHandler = new RocketMQProducerMessageHandler(destination, extendedProducerProperties, mqProducerProperties);
messageHandler.setApplicationContext(this.getApplicationContext());
PartitioningInterceptor partitioningInterceptor = ...;
messageHandler.setPartitioningInterceptor(partitioningInterceptor);
messageHandler.setBeanFactory(this.getApplicationContext().getBeanFactory());
messageHandler.setErrorMessageStrategy(this.getErrorMessageStrategy());
return messageHandler;
}
}
在RocketMQProducerMessageHandler中,可以看到很多RocketMQ原生的东西,例如启动RocketMQ、发送消息、事务消息等,在发送消息前,首先将Spring的消息Message转换为RocketMQ的消息org.apache.rocketmq.common.message.Message,然后在根据配置发送普通消息或事务消息。
public class RocketMQProducerMessageHandler extends AbstractMessageHandler {
private DefaultMQProducer defaultMQProducer;
public void start() {
// 启动DefaultMQProducer
this.defaultMQProducer.start();
}
...
protected void handleMessageInternal(Message<?> message) {
try {
// 启动DefaultMQProducer
org.apache.rocketmq.common.message.Message mqMessage = RocketMQMessageConverterSupport.convertMessage2MQ(this.destination.getName(), message);
Object sendResult;
if (this.defaultMQProducer instanceof TransactionMQProducer) {
// 发送消息
sendResult = this.defaultMQProducer.sendMessageInTransaction(mqMessage, message.getHeaders().get("TRANSACTIONAL_ARGS"));
} else {
// 发送消息
sendResult = this.send(mqMessage, this.messageQueueSelector, message.getHeaders(), message);
}
}
}
}
(2)消息消费
对于消息消费,RocketMQ提供了RocketMQMessageChannelBinder,该类主要封装了RocketMQInboundChannelAdapter来做消息消费和RocketMQ的适配。
public class RocketMQMessageChannelBinder {
protected MessageProducer createConsumerEndpoint(...) throws Exception {
RocketMQInboundChannelAdapter inboundChannelAdapter = new RocketMQInboundChannelAdapter(destination.getName(), extendedConsumerProperties);
...
return inboundChannelAdapter;
}
}
Channel Adapter是Spring Integration中的一个核心组件,在其consumeMessage方法中,首先将消息从RocketMQ消息体转换为Spring的消息体,然后将消息发送到channel中。
public class RocketMQInboundChannelAdapter {
private <R> R consumeMessage(List<MessageExt> messageExtList, Supplier<R> failSupplier, Supplier<R> sucSupplier) {
Iterator iterator = messageExtList.iterator();
while (iterator.hasNext()) {
MessageExt messageExt = (MessageExt) iterator.next();
try {
// 实现消息转换
Message<?> message = RocketMQMessageConverterSupport.convertMessage2Spring(messageExt);
if (this.retryTemplate != null) {
this.retryTemplate.execute((context) -> {
this.sendMessage(message); // 发送消息
return message;
}, this.recoveryCallback);
} else {
this.sendMessage(message);
}
}
} return sucSupplier.get();
}
}
2、RabbitMQ集成
对于RabbitMQ,其又Exchange和Queue,生产者将消息发送到Exchange,然后消费者从Queue中消费消息,而Exchange和Queue使用Routing Key进行绑定。
(1)消息发布
RabbitMQ提供了RabbitMessageChannelBinder类,重写了createProducerMessageHandler方法,在该方法中创建了一个AmqpOutboundEndpoint。
public class RabbitMessageChannelBinder {
protected MessageHandler createProducerMessageHandler(...) {
String exchangeName = producerDestination.getName();
// 确定Exchange和Destination
String destination = StringUtils.isEmpty(prefix) ? exchangeName : exchangeName.substring(prefix.length());
Object endpoint;
if (...){
...
} else{
// 创建AmqpOutboundEndpoint
endpoint = this.amqpHandler(producerDestination, producerProperties, errorChannel, destination, extendedProperties);
}
return (MessageHandler) endpoint;
}
}
在AmqpOutboundEndpoint中,提供了消息发送方法send,其逻辑也是先将Spring消息体转换为RabbitMQ消息体org.springframework.amqp.core.Message,然后使用RabbitTemplate进行发送。
public class AmqpOutboundEndpoint extends AbstractAmqpOutboundEndpoint {
private final RabbitTemplate rabbitTemplate;
private void send(String exchangeName, String routingKey, final Message<?> requestMessage, CorrelationData correlationData) {
if (this.rabbitTemplate != null) {
// 使用RabbitTemplate发送消息
this.doRabbitSend(exchangeName, routingKey, requestMessage, correlationData, this.rabbitTemplate);
}
}
private void doRabbitSend(String exchangeName, String routingKey, final Message<?> requestMessage, CorrelationData correlationData, RabbitTemplate template) {
MessageConverter converter = template.getMessageConverter();
// 使用RabbitTemplate发送消息
org.springframework.amqp.core.Message amqpMessage = ...;
// 发送消息
template.send(exchangeName, routingKey, amqpMessage, correlationData);
}
}
(2)消费消息
对于消息消费,RabbitMQ提供了RabbitMessageChannelBinder,重写了createConsumerEndpoint方法,在该方法中也是返回了AmqpInboundChannelAdapter适配器。
public class RabbitMessageChannelBinder {
protected MessageProducer createConsumerEndpoint(...) {
String destination = consumerDestination.getName();
MessageListenerContainer listenerContainer = ...;
// 创建InboundChannelAdapter
AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(listenerContainer);
adapter.setBindSourceMessage(true);
adapter.setBeanFactory(this.getBeanFactory());
adapter.setBeanName("inbound." + destination);
return adapter;
}
}
在AmqpInboundChannelAdapter中,也是将RabbitMQ消息体转换为Spring消息体,并将消息发送到channel通道中。
public class AmqpInboundChannelAdapter extends MessageProducerSupport {
protected class Listener implements ChannelAwareMessageListener {
public void onMessage(final Message message, final Channel channel) {
try {
if (this.retryOps == null) {
this.createAndSend(message, channel);
} else {
// 实现消息转换
org.springframework.messaging.Message<Object> toSend = this.createMessageFromAmqp(message, channel);
this.retryOps.execute((context) -> {
StaticMessageHeaderAccessor.getDeliveryAttempt(toSend).incrementAndGet();
AmqpInboundChannelAdapter.this.setAttributesIfNecessary(message, toSend);
// 发送消息
AmqpInboundChannelAdapter.this.sendMessage(toSend);
return null;
}, this.recoverer);
}
}
}
}
}
五、使用Spring Cloud Gateway实现API网关
(一)服务网关基本概念
1、服务网关的必要性
服务入口:在客户端和服务端之间,需要一个统一的访问入口,如果一个客户端有多个服务端的入口,是不太合适的,因此需要一个网关来做统一入口。
服务粒度:如果没有网关,有的客户端对应两个服务端,有的对应三个服务端,这样每个客户端的粒度就不一样了,对于不同的客户端粒度不一样,控制力就会很差,服务端发生变更,客户端就需要重构,因此需要网关来控制统一的粒度。
服务隔离:当客户端访问服务端时,不太想将服务端底层的一些内容暴露给客户端,因为服务端对客户端应该是透明的,客户端知道服务端可以进行访问,至于服务端在哪,客户端不需要关心,例如服务端升级后,版本从v1升级到v2,客户端是不需要关心的,是服务端自己需要处理的,这就需要网关来做隔离。
2、服务网关作用
服务网关的作用类似于门面模式,就是对所有的服务提供统一的API访问,作用有解耦、适配、数据聚合。
解耦和适配不必多说,对于数据聚合,例如APP和PC端的需求不一样,但是服务端只是提供服务接口,至于怎么用,和服务端没什么关系,因此就需要网关来做不同的处理,例如数据需要从多个系统获取并组合返回,或者获取后根据不同的入口做定制化返回等。
3、服务网关结构和功能
服务网关从结构上分为入口、网关和后端服务。
网关提供了请求管理、业务路由、日志记录、安全管理、访问控制、服务适配等功能。
(二)Spring Cloud Gateway功能特性
Spring Cloud Gateway中最主要的就是路由,路由中包含Filter拦截器和Predicare谓词。
Spring Cloud Gateway的核心功能是对Web请求进行路由和过滤,其内部大量依赖于Spring中的响应式Web框架WebFlux。
Spring Cloud Gateway执行流程如下图所示,当客户端请求到达时,首先经过DispatcherHandler分发请求,然后RoutePredicateHandlerMapping中的lookupRoute进行断言,如果断言失败,则执行下一个断言,如果断言成功,则执行过滤链执行过滤操作。
Spring Cloud Gateway配置 - 组成结构:
Route(路由) :网关的基本构建块,由 ID、目标URI、谓词集合和过滤器集合定义
Predicates(谓词或断言) :匹配HTTP请求中的所有内容,例如消息头或参数。符合谓词规则才能通过
Filter(过滤器) :对请求过程进行拦截,添加定制化处理机制
配置样例如下图所示:
spring:
main:
web-application-type: reactive # 以响应式方式运行Web应用,防止与MVC冲突
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用服务名进行路由
lower-case-service-id: true # 用小写的请求路径的服务名匹配服务,默认为false
routes:
- id: baiduroute
uri: https://www.baidu.com
predicates:
- Path=/baidu/**
- id: chat-service
uri: lb://chat-service # 指定URI,集成负载均衡机制
predicates:
- Path=/chat/** # 指定谓词,当请求URL中包含 /chat/**时会被 自动转发到chat-service
filters:
- StripPrefix=1
- PrefixPath=/chats # 指定过滤器
Spring Cloud Gateway中内置了很多谓词,如下代码所示,就是其内置的谓词
#Spring Cloud Gateway启动日志:
Loaded RoutePredicateFactory[After]
Loaded RoutePredicateFactory[Before]
Loaded RoutePredicateFactory[Between]
Loaded RoutePredicateFactory[Cookie]
Loaded RoutePredicateFactory[Header]
Loaded RoutePredicateFactory[Host]
Loaded RoutePredicateFactory[Method]
Loaded RoutePredicateFactory[Path] # 路径谓词最常用
Loaded RoutePredicateFactory[Query]
Loaded RoutePredicateFactory[ReadBody]
Loaded RoutePredicateFactory[RemoteAddr]
Loaded RoutePredicateFactory[XForwardedRemoteAddr]
Loaded RoutePredicateFactory[Weight]
Loaded RoutePredicateFactory[CloudFoundryRouteService]
在这么多谓词中,最常用的是Path,即根据路径做断言,其实现源码如下所示,主要逻辑就是基于模式匹配实现谓词规则判断,即判断path是否可以匹配,如果匹配成功,则调整URI路径。
public class PathRoutePredicateFactory ...{
public Predicate<ServerWebExchange> apply(PathRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
public boolean test(ServerWebExchange exchange) {
PathPattern match = null;
for (int i = 0; i < pathPatterns.size(); ++i) {
PathPattern pathPattern = (PathPattern) pathPatterns.get(i);
// 基于模式匹配实现谓词规则判断
if (pathPattern.matches(path)) {
match = pathPattern;
break;
}
}
if (match != null) {
PathRoutePredicateFactory.traceMatch("Pattern", match.getPatternString(), path, true);
PathMatchInfo pathMatchInfo = match.matchAndExtract(path);
// 如果匹配成功则调整URI路径
ServerWebExchangeUtils.putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
return true;
}
}
};
}
}
Spring Cloud Gateway中也内置了一些过滤器,如下代码所示。
#Spring Cloud Gateway网关过滤器
AddRequestHeader
AddRequestParameter
AddResponseHeader
DedupeResponseHeader
MapRequestHeader
Prefix
PreserveHostHeader
RemoveRequestHeader
RemoveResponseHeader
RemoveRequestParameter # GatewayFilter:只作用于单个路由
RewritePath
StripPrefix
RequestSize
一个路由的配置样例如下所示,首先使用id指定路由器名称、然后使用uri指定URI,uri可以使用 lb 集成负载均衡;使用predicates来指定谓词来做断言;使用filters来执行过滤器,以下面代码为例,StripPrefix去除URI的第一个路径,PrefixPath在URI上添加一个/chats路径。
假设chat-service的地址是localhost:12002/chats/,网关地址是localhost:18080,那么访问localhost:18080/chat/相当于是localhost:12002/chats/,去掉chat段路径,再添加chats前缀。
- id: chat-service
uri: lb://chat-service # 指定URI,集成负载均衡机制
predicates:
- Path=/chat/** # 指定谓词,当请求URL中包含 /chat/**时会被 自动转发到chat-service
filters:
- StripPrefix=1
- PrefixPath=/chats # 指定过滤器
StripPrefix过滤器的实现源码如下所示,可以看到,首先从URI中获取路径,然后使用 / 分割路径,然后从下标为1开始重新拼接路径。
public class StripPrefixGatewayFilterFactory ...{
public GatewayFilter apply(StripPrefixGatewayFilterFactory.Config config) {
return new GatewayFilter() {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {...
String path = request.getURI().getRawPath();
String[] originalParts = StringUtils.tokenizeToStringArray(path, "/");
StringBuilder newPath = new StringBuilder("/");
for (int i = 0; i < originalParts.length; ++i) {
if (i >= config.getParts()) {
if (newPath.length() > 1) {
newPath.append('/');
}
// 根据配置构建新路径
newPath.append(originalParts[i]);
}
}
// 通过新路径执行访问
return chain.filter(exchange.mutate().request(newRequest).build());
}
};
}
}
在spring boot actuator中,集成了查看路由的端点:http://localhost:18080/actuator/gateway/routes
[
{
"predicate":"Paths: [/baidu/**], match trailing slash: true",
"route_id":"baiduroute",
"filters":[
],
"uri":"https://www.baidu.com:443",
"order":0
},
{
"predicate":"Paths: [/chat/**], match trailing slash: true",
"route_id":"chat-service",
"filters":[
"[[StripPrefix parts = 1], order = 1]",
"[[PrefixPath prefix = '/chats'], order = 2]"
],
"uri":"lb://chat-service",
"order":0
}
]
(三)Spring Cloud Gateway整合和扩展
1、过滤器整合
Spring Cloud Gateway有针对单个路由的GatewayFilter和全局过滤器GlobalFilter,其中GatewayFilter 通过配置作用于单个路由,而GlobalFilter则作用于所有的请求当请求匹配到对应路由时,会将GlobalFilter和已绑定路由的GatewayFilter合并到一起。
如下代码所示,在FilteringWebHandler中,首先从Route中获取GatewayFilter集合,然后再将过滤器集合和全局过滤器集合合并并排序。
public class FilteringWebHandler implements WebHandler {
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = (Route) exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
// 从Route中获取GatewayFilter
List<GatewayFilter> gatewayFilters = route.getFilters();
List<GatewayFilter> combined = new ArrayList(this.globalFilters);
// 将GlobalFilter和GatewayFilter合并 GlobalFilter会被转换为GatewayFilter
combined.addAll(gatewayFilters);
AnnotationAwareOrderComparator.sort(combined);
// 创建FilterChain并执行过滤
return (new FilteringWebHandler.DefaultGatewayFilterChain(combined)).filter(exchange);
}
}
过滤器链 - DefaultGatewayFilterChain
private static class DefaultGatewayFilterChain implements GatewayFilterChain {
private final List<GatewayFilter> filters;
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < this.filters.size()) {
GatewayFilter filter = (GatewayFilter) this.filters.get(this.index);
FilteringWebHandler.DefaultGatewayFilterChain chain = new FilteringWebHandler.DefaultGatewayFilterChain(this, this.index + 1);
// 执行过滤器链
return filter.filter(exchange, chain);
} else {
return Mono.empty();
}
});
}
}
过滤器示例 - RouteToRequestUrlFilter:
一个过滤器的典型实现过程基本就是从ServerWebExchange上下文中获取路由,然后根据路由详细信息来执行对应的操作。
public class RouteToRequestUrlFilter implements GlobalFilter, Ordered {
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取路由信息
Route route = (Route) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
if (route == null) {
return chain.filter(exchange);
} else {
// 获取请求和路由中的URI
URI uri = exchange.getRequest().getURI();
URI routeUri = route.getUri();
...
// 如果RouteUri以lb开头,则请求中必须带有host,否则直接抛出异常
if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) { ...} else {
URI mergedUrl = ...;
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl);
// 生成请求URL放入上下文中,执行过滤器链
return chain.filter(exchange);
}
}
}
}
2、自定义过滤器
除了上面提到内置的过滤器,我们也可以做自定义过滤器的开发
(1)如下代码所示,自定义一个JWTAuthFilter的GlobalFilter,主要用来为HTTP请求添加一个消息头,用来设置与访问Token相关的安全认证信息。
// 实现GlobalFilter 接口
@Configuration
public class JWTAuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
// HTTP请求添加一个消息头,用来设置与访问Token相关的安全认证信息
builder.header("Authorization", "JWTToken");
chain.filter(exchange.mutate().request(builder.build()).build());
return chain.filter(exchange.mutate().request(builder.build()).build());
}
}
(2)自定义GatewayFilter
自定义GatewayFilter需要继承AbstractGatewayFilterFactory并重写apply方法,在apply方法中,首先从过滤链中获取ServerHttpResponse,然后再针对Response的做各种处理。
// 扩展AbstractGatewayFilterFactory
public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory {
public PostGatewayFilterFactory() {
super(Config.class);
}
public GatewayFilter apply() {
return apply(o -> {
});
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 添加针对Response的各种处理
ServerHttpResponse response = exchange.getResponse();
...
}));
};
}
// 读取路由配置信息
public static class Config {
}
}
3、内置过滤器 - 限流过滤器
在Spring Cloud Gateway中内置了很多过滤器,这里以限流过滤器为例,name是RequestRateLimiter,其使用了令牌桶限流算法,然后需要设置流量速度和桶大小。
spring:
cloud:
gateway:
routes:
- id: requestratelimiterroute
uri: lb://testservice
filters:
- name: RequestRateLimiter # 请求限流器配置
args:
redis-rate-limiter.replenishRate: 50 # 流量速度和桶大小参数
redis-rate-limiter.burstCapacity: 100
在其实现中,也是重写了apply方法,在方法中,首先获取获取限流器对象RateLimiter,然后调用RateLimiter的isAllowe方法,判断是否被限流,如果未被限流则允许访问,提交过滤器链继续过滤;如果被限流则不允许访问,设置响应状态码429。
@Override
public GatewayFilter apply(Config config) {
// 获取限流器对象RateLimiter
RateLimiter<Object> limiter = getOrDefault(config.rateLimiter, defaultRateLimiter);
return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
// 调用RateLimiter的isAllowe方法,判断是否被限流
return limiter.isAllowed(routeId, key).flatMap(response -> {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
// 未被限流则允许访问,提交过滤器链继续过滤
if (response.isAllowed()) {
return chain.filter(exchange);
}
// 被限流则不允许访问,设置响应状态码429
setResponseStatus(exchange, config.getStatusCode());
return exchange.getResponse().setComplete();
}
}
}
isAllowed首先获取限流器相关参数,生成Lua脚本,最后通过RedisTemplate模板工具类 执行Lua脚本,获取令牌。
public Mono<Response> isAllowed(String routeId, String id) {
int replenishRate = routeConfig.getReplenishRate();
// 获取限流器相关参数
int burstCapacity = routeConfig.getBurstCapacity();
try {
List<String> keys = getKeys(id);
// 获得Lua脚本参数
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// 通过RedisTemplate模板工具类 执行Lua脚本,获取令牌
Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L))).reduce(new ArrayList<Long>(), (longs, l) -> {
longs.addAll(l);
return longs;
})...;
}]return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
}
4、Spring Cloud Gateway功能扩展
Spring Cloud Gateway功能扩展包括异常处理、日志管理、灰度发布、安全控制等。
(1)日志管理
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getPath().pathWithinApplication().value();
Route route = getGatewayRoute(exchange);
String ipAddress = WebUtils.getServerHttpRequestIpAddress(request);
GatewayLog gatewayLog = new GatewayLog();
gatewayLog.setSchema(request.getURI().getScheme());
gatewayLog.setRequestMethod(request.getMethodValue());
gatewayLog.setRequestPath(requestPath);
gatewayLog.setTargetServer(route.getId());
gatewayLog.setRequestTime(new Date());
gatewayLog.setIp(ipAddress);
return writeBLog(exchange, chain, gatewayLog);
}
}
(2)灰度发布
Weight={group}, {weigth},group:需要分流的组名,weigth:流量百分比,最大值100
spring:
cloud:
gateway:
routes:
- id: old-customer-service
uri: lb://old-customer-service
predicates:
- Path=/old/**
#old-customer-service的流量权重
- Weight=customer-group, 99
- id: new-customer-service
uri: lb://new-customer-service
predicates:
- Path=/new/**
#new-customer-service流量权重
- Weight=customer-group, 1
(四)客服系统案例演进
1、添加网关
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加注册中、服务、日志等配置
server:
port: 18080
management:
endpoints:
web:
exposure:
include: "*"
spring:
application:
name: gateway-server
cloud:
nacos:
discovery:
server-addr: 192.168.249.130:8848
namespace: dae2f8c4-a44a-4143-afc5-1f8aaa84c72c
group: LCL_GALAXY_GROUP
cluster-name: beijing
logging:
level:
org.springframework.cloud.gateway: DEBUG
添加路由等配置
spring:
main:
web-application-type: reactive # 以响应式方式运行Web应用,防止与MVC冲突
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用服务名进行路由
lower-case-service-id: true # 用小写的请求路径的服务名匹配服务,默认为false
routes:
- id: baiduroute
uri: https://www.baidu.com
predicates:
- Path=/baidu/**
- id: ticket-service
uri: lb://ticket-service # 指定URI,集成负载均衡机制
predicates:
- Path=/ticket/** # 指定谓词,当请求URL中包含 /ticket/**时会被 自动转发到ticket-service
filters:
- StripPrefix=1
- id: chat-service
uri: lb://chat-service # 指定URI,集成负载均衡机制
predicates:
- Path=/chat/** # 指定谓词,当请求URL中包含 /chat/**时会被 自动转发到chat-service
filters:
- StripPrefix=1
- PrefixPath=/chats # 指定过滤器
访问监控端点http://localhost:18080/actuator/gateway/routes,可以查看配置的路由信息,同时Gateway也有自动路由,即不需要配置,会根据服务名称自动配置路由信息,例如下面的/gateway-server/**。
[{"predicate":"Paths: [/gateway-server/**], match trailing slash: true","metadata":{"nacos.instanceId":"10.136.213.206#18080#beijing#LCL_GALAXY_GROUP@@gateway-server","nacos.weight":"1.0","nacos.cluster":"beijing","nacos.ephemeral":"true","nacos.healthy":"true","preserved.register.source":"SPRING_CLOUD"},"route_id":"ReactiveCompositeDiscoveryClient_gateway-server","filters":["[[RewritePath /gateway-server/?(?<remaining>.*) = '/${remaining}'], order = 1]"],"uri":"lb://gateway-server","order":0},{"predicate":"Paths: [/chat-service/**], match trailing slash: true","metadata":{"nacos.instanceId":"10.136.213.206#9004#beijing#LCL_GALAXY_GROUP@@chat-service","nacos.weight":"1.0","nacos.cluster":"beijing","nacos.ephemeral":"true","nacos.healthy":"true","preserved.register.source":"SPRING_CLOUD"},"route_id":"ReactiveCompositeDiscoveryClient_chat-service","filters":["[[RewritePath /chat-service/?(?<remaining>.*) = '/${remaining}'], order = 1]"],"uri":"lb://chat-service","order":0},{"predicate":"Paths: [/ticket-service/**], match trailing slash: true","metadata":{"nacos.instanceId":"10.136.213.206#9005#beijing#LCL_GALAXY_GROUP@@ticket-service","nacos.weight":"1.0","nacos.cluster":"beijing","nacos.ephemeral":"true","nacos.healthy":"true","preserved.register.source":"SPRING_CLOUD"},"route_id":"ReactiveCompositeDiscoveryClient_ticket-service","filters":["[[RewritePath /ticket-service/?(?<remaining>.*) = '/${remaining}'], order = 1]"],"uri":"lb://ticket-service","order":0},{"predicate":"Paths: [/baidu/**], match trailing slash: true","route_id":"baiduroute","filters":[],"uri":"https://www.baidu.com:443","order":0},{"predicate":"Paths: [/ticket/**], match trailing slash: true","route_id":"ticket-service","filters":["[[StripPrefix parts = 1], order = 1]"],"uri":"lb://ticket-service","order":0},{"predicate":"Paths: [/chat/**], match trailing slash: true","route_id":"chat-service","filters":["[[StripPrefix parts = 1], order = 1]","[[PrefixPath prefix = '/chats'], order = 2]"],"uri":"lb://chat-service","order":0}]
访问测试:
###
GET http://localhost:18080/baidu
Accept: application/json
###
# 基于Gateway动态路由:自动通过服务名称来确定服务路由
GET http://localhost:18080/chat-service/chats/
Accept: application/json
###
# 基于Gateway自定义路路由:去掉URI的第一个(chat),然后在URI前面添加chats
GET http://localhost:18080/chat/
Accept: application/json
###
POST http://localhost:18080/ticket-service/customerTickets/
Content-Type: application/json
{
"userId" : "100",
"staffId" : "2025",
"inquire" : "你好,测试"
}
2、基于网关实现全局异常处理
要实现全局异常处理,就需要实现ErrorWebExceptionHandler接口并重写handle方法。另外就是需要使用@Order注解,将序号调低,保证优先级比系统默认的异常处理器要高,确保出现异常时优先进入自定义处理流程中。
@Component
@Slf4j
@Order(-1) // 保证优先级比系统默认的异常处理器要高,确保出现异常时优先进入自定义处理流程中
public class GlobalExecptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if(response.isCommitted()){
return Mono.error(ex);
}
// 转换为自定义的result
Result<?> result;
if(ex instanceof ResponseStatusException){
// 处理网关自动抛出的异常
result = responseStatusExceptionHandler(exchange, (ResponseStatusException) ex);
} else {
// 处理其他异常
result = globalExceptionHandler(exchange, ex);
}
return writeResult(exchange, result);
}
Result<?> responseStatusExceptionHandler(ServerWebExchange exchange, ResponseStatusException ex){
ServerHttpRequest request = exchange.getRequest();
log.error("路径:{} 发生异常,方法为:{}", request.getURI(), ex);
return Result.error(ex.getRawStatusCode(), ex.getReason());
}
Result<?> globalExceptionHandler(ServerWebExchange exchange, Throwable ex){
ServerHttpRequest request = exchange.getRequest();
log.error("路径:{} 发生异常,方法为:{}", request.getURI(), ex);
return Result.error(500, "网关发生异常");
}
public Mono<Void> writeResult(ServerWebExchange exchange, Object object) {
// 设置 header
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
// 设置 Body
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
return bufferFactory.wrap(JSON.toJSONBytes(object));
} catch (Exception ex) {
ServerHttpRequest request = exchange.getRequest();
log.error("[writeResult][uri({}/{})发生异常:{}]", request.getURI(), request.getMethod(), ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~