SpringCloud Gateway的核心概念
SpringCloud Gateway是Spring官方最新推出的一款基于SpringFramework5,Project Reactor和SpringBoot 2之上开发的网关。
它与第一代网关Zuul不同的是:gateway是异步非阻塞的(netty + webflux实现);zuul是同步阻塞请求的
Gateway由三大组成部分,分别是路由、断言、过滤器。
谓词Predicate
谓词Predicate对于熟悉Java8函数式编程的开发者来说并不会陌生,这里不做累述。简单地理解,predicate在gateway中是路径相匹配的进行路由。
静态路由配置与动态路由配置
静态路由的配置
静态路由配置写在配置文件中(yml或者properties文件中),端点是:spring.cloud.gateway
静态路由的缺点非常明显,每次改动都要重新部署网关模块。
动态路由配置
路由信息在Nacos中维护,可以实现动态变更
Gateway集成Alibaba Nacos实现动态路由配置
第一步:启动nacos,创建配置文件,如下图所示:
具体的配置信息如下:
[
{
"id": "e-commerce-nacos-client-imooc",
"predicates": [
{
"args": {
"pattern": "/imooc/ecommerce-nacos-client/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-nacos-client"
}
]
复制代码
第二步:创建Gateway工程
引入的依赖有:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
复制代码
bootstrap.yml的配置如下:
server:
port: 9001
servlet:
context-path: /imooc
spring:
application:
name: e-commerce-gateway
cloud:
nacos:
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
namespace: fa2e39fc-7e9b-4b8c-bd35-53543d2e1d8a
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
# 静态路由
# gateway:
# routes:
# - id: path_route # 路由的ID
# uri: 127.0.0.1:8080/user/{id} # 匹配后路由地址
# predicates: # 断言, 路径相匹配的进行路由
# - Path=/user/{id}
main:
allow-bean-definition-overriding: true # 因为将来会引入很多依赖, 难免有重名的 bean
# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
nacos:
gateway:
route:
config:
data-id: e-commerce-gateway-router
group: e-commerce
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
复制代码
第三步:在gateway微服务应用中实现一个配置类,用来读取nacos上的配置信息。
/**
* 配置类,用来读取nacos相关的配置项,用于配置***
*/
@Configuration
public class GatewayConfig {
/**
* 读取配置的超时时间
*/
public static final long DEFAULT_TIMEOUT = 30000;
public static String NACOS_SERVER_ADDR;
/**
* 命名空间
*/
public static String NACOS_NAMESPACE;
/**
* data-id
*/
public static String NACOS_ROUTE_DATA_ID;
public static String NACOS_ROUTE_GROUP;
@Value("${spring.cloud.nacos.discovery.server-addr}")
public void setNacosServerAddr(String nacosServerAddr){
NACOS_SERVER_ADDR = nacosServerAddr;
}
@Value("${spring.cloud.nacos.discovery.namespace}")
public void setNacosNamespace(String nacosNamespace){
NACOS_NAMESPACE = nacosNamespace;
}
@Value("${nacos.gateway.route.config.data-id}")
public void setNacosRouteDataId(String nacosRouteDataId){
NACOS_ROUTE_DATA_ID = nacosRouteDataId;
}
@Value("${nacos.gateway.route.config.group}")
public void setNacosRouteGroup(String nacosRouteGroup){
NACOS_ROUTE_GROUP = nacosRouteGroup;
}
}
复制代码
第四步:定义一个service的类,DynamicRouteServiceImpl,实现ApplicationEventPublisherAware接口。
ApplicationEventPublisherAware接口是Spring的事件机制,实现这个类需要实现句柄的初始化,而句柄是用来推送消息的。事件机制有点想消息队列以及kafka,即可以推送消息,也有监听者进行监听,一旦监听者监听到消息就会处理相应的逻辑。
推送消息到gateway的路由配置只需要一行代码:
this.publisher.publishEvent(new RefreshRoutesEvent(this));
复制代码
我们可以看到RefreshRoutesEvent就是我们要推送的事件的一个类,如果是我们自己定义的事件推送的话,就需要我们自己定义事件类,并且定义一个监听类来监听这个事件。 但是在Spring Cloud中已经帮我们实现了RefreshRoutesEvent这个事件类以及***RouteRefreshListener。 在源码中如下图所示:
所以说,我们推送消息到网关中的时候只需要上面的一行代码就可以了。
更新route路由的实现类具体代码如下:
**
* 事件推送Aware:动态更新路由网关Service
*/
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
/**
* 写路由定义
*/
private final RouteDefinitionWriter routeDefinitionWriter;
/**
* 获取路由定义
*/
private final RouteDefinitionLocator routeDefinitionLocator;
/**
* 事件发布
*/
private ApplicationEventPublisher publisher;
public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,
RouteDefinitionLocator routeDefinitionLocator){
this.routeDefinitionWriter = routeDefinitionWriter;
this.routeDefinitionLocator = routeDefinitionLocator;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher
applicationEventPublisher) {
// 完成事件推送句柄的初始化
this.publisher = applicationEventPublisher;
}
/**
* 增加路由定义
* @param definition
* @return
*/
public String addRouteDefinition(RouteDefinition definition){
log.info("gateway add route: [{}]",definition);
//保存路由配置并发布
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* 更新路由
* @param definitions
* @return
*/
public String updateList(List<RouteDefinition> definitions){
log.info("gateway update route: [{}]",definitions);
//先拿到当前Gateway 中存储的路由定义
List<RouteDefinition> routeDefinitionsExits =
routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
if (!CollectionUtils.isEmpty(routeDefinitionsExits)){
//清除掉之前所有的“旧的”路由定义
routeDefinitionsExits.forEach(rd ->{
log.info("delete route definition: [{}]",rd);
deleteById(rd.getId());
});
//把更新的路由定义同步到gateway中
definitions.forEach(definition -> updateByRouteDefinition(definition));
}
return "success";
}
/**
* 根据路由id删除路由配置
*/
private String deleteById(String id){
try {
log.info("gateway delete route id: [{}]",id);
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
//发布事件通知给gateway 更新路由定义
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "delete success";
}catch (Exception ex){
log.error("gateway delete route fail:[{}]",ex.getMessage(),ex);
return "delete fail";
}
}
/**
* 更新路由
* 更新的实现策略比较简单: 删除 + 新增 = 更新
* @param definition
* @return
*/
private String updateByRouteDefinition(RouteDefinition definition){
try{
log.info("gateway update route: [{}]",definition);
this.routeDefinitionWriter.delete(Mono.just(definition.getId())).subscribe();
}catch (Exception ex){
return "update fail, not find route routeId: " + definition.getId();
}
try{
this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "update success";
}catch (Exception ex){
return "update route fail";
}
}
}
复制代码
我们在该类中定义了写路由定义routeDefinitionWriter
,用来往路由中进行更新以及保存的操作。为了能够读取路由的配置,我们还定义了读路由定义routeDefinitionLocator
,用来读取路由的相关配置,因为在更新路由的时候我们要先读取旧的路由配置,然后把就的路由配置删除,再写入新的路由配置,所以需要用来读路由的配置。
第五步:定义一个类来监听Nacos上配置文件的变化
我们定义一个类叫做DynamicRouteServiceImplByNacos
,其中这个类的上面有一个注解是
@DependsOn({"gatewayConfig"})
复制代码
表示这个类依赖gatewayConfig这个bean,就是说先有gatewayConfig这个bean,因为我们要读取nacos上的配置信息。
其中需要两个类变量,分别是
/**
* Nacos 配置服务
*/
private ConfigService configService;
private final DynamicRouteServiceImpl dynamicRouteService;
// 通过构造方法注入
public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
this.dynamicRouteService = dynamicRouteService;
}
复制代码
DynamicRouteServiceImpl就是我们刚才实现的那个类。 configService是Nacos配置服务。需要我们自己实现一个方法去初始化,而不是通过注入的形式。 首先,我们定义初始化Nacos Config配置的方法。
/**
* 初始化 Nacos Config
*/
private ConfigService initConfigService(){
try{
Properties properties = new Properties();
properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);
properties.setProperty("namespace",GatewayConfig.NACOS_NAMESPACE);
return configService = NacosFactory.createConfigService(properties);
}catch (Exception ex){
log.error("init gateway nacos config error: [{}]",ex.getMessage(),ex);
return null;
}
}
复制代码
我们再定一个方法区监听Nacos上指定配置文件的方法。
/**
* 监听 Nacos 下发的动态路由配置
* @param dataId
* @param group
*/
private void dynamicRouteByNacosListener(String dataId, String group){
try {
//给 Nacos Config客户端增加一个***
configService.addListener(dataId, group, new Listener() {
/**
* 自己提供线程池执行操作
* @return
*/
@Override
public Executor getExecutor() {
return null;
}
/**
* ***收到配置更新
* @param configInfo Nacos 中最新的配置定义
*/
@Override
public void receiveConfigInfo(String configInfo) {
log.info("start to update config: [{}]", configInfo);
List<RouteDefinition> definitionList =
JSON.parseArray(configInfo,RouteDefinition.class);
log.info("update route:[{}]",definitionList.toString());
dynamicRouteService.updateList(definitionList);
}
});
}catch (NacosException ex){
log.error("dynamic update gateway config error: [{}]", ex.getMessage(),ex);
}
}
复制代码
我们可以看到确定唯一的配置文件只需要dataId和group。在receiveConfigInfo就是***收到配置更新的方法,我们的配置文件是json格式的数组,获取到之后就可以调用我们之前写好的更新的方法。
我们同时要将这个类定义为Component,在完成这个bean的初始化之后,我们要执行一个init的方法,在方法上使用注解@PostConstruct来执行这个方法,就是在bean初始化之后就执行。
/**
* Bean在容器中构造完成之后会执行 init 方法
*/
@PostConstruct
public void init(){
log.info("gateway route init....");
try{
//初始化 Nacos 配置客户端
configService = initConfigService();
if (null == configService){
log.error("init config service fail");
return;
}
//通过 Nacos Config 并指定路由配置去获取路由配置
String configInfo = configService.getConfig(
GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP,
GatewayConfig.DEFAULT_TIMEOUT
);
log.info("get current gateway config: [{}]", configInfo);
List<RouteDefinition> definitionList =
JSON.parseArray(configInfo, RouteDefinition.class);
if (CollectionUtil.isNotEmpty(definitionList)){
for (RouteDefinition definition : definitionList){
log.info("init gateway config: [{}]",definition.toString());
dynamicRouteService.addRouteDefinition(definition);
}
}
}catch (Exception ex){
log.error("gateway route init has some error:[{}]", ex.getMessage(), ex);
}
// 设置***
dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID,
GatewayConfig.NACOS_ROUTE_GROUP);
}
复制代码
我们启动gateway服务的时候,查看启动的日志,可以看到如下:
这就意味着能够成功读取到配置文件。
尝试在nacos上修改配置文件,看下能否监听到配置文件的修改。
发现是可以监听到的。 以上就是动态监听路由配置的实现。
原文链接:https://juejin.cn/post/7053336525467025439