GateWay动态路由
在Gateway的学习过程中都是基于配置文件(application.yml)的方式静态加载路由,而在实际的生产环境中静态的配置往往不能够满足实际的开发需求,所以需要一个存储终端持久化所需要的路由信息,并且支持实时加载更新。
本文采用 监听 Nacos 配置文件读取到JSON将其转为 RouteDefinition 对象,配合 RouteDefinitionWriter 及 ApplicationEventPublisher 实例进行路由刷新, 实现动态路由
探讨目的
- 深入了解 Gateway 内部路由加载逻辑
- 巩固 Nacos-Client 核心底层操作
- 基于 Nacos 2.x 实现 Gateway 的动态路由加载
Gateway RouteLocator 源码导读
在 Spring-Cloud-Gateway 为了进行路由的加载操作,在自动装配类中存在一个路由定位器 RouteLocator
@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties, List<GatewayFilterFactory> gatewayFilters, List<RoutePredicateFactory> predicates, RouteDefinitionLocator routeDefinitionLocator, ConfigurationService configurationService) {
return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, gatewayFilters, properties, configurationService);
}
@Bean
@Primary
@ConditionalOnMissingBean(name = {"cachedCompositeRouteLocator"})
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
public interface RouteLocator {
Flux<Route> getRoutes();
}
通过方法定义可以看出他最终内部保存的路由是
Route对象实例
RouteLocator 的三个实现类分别为:
- RouteDefinitionRouteLocator:路由基础定义
- CachingRouteLocator:缓存路由定位器
- CompositeRouteLocator:复合路由
本文主要以
RouteDefinitionRouteLocator源代码,来解释在Gateway中内部路由是如何存储的,以下是截取源代码
package org.springframework.cloud.gateway.route;
public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware {
public static final String DEFAULT_FILTERS = "defaultFilters";
protected final Log logger = LogFactory.getLog(this.getClass());
private final RouteDefinitionLocator routeDefinitionLocator;
private final ConfigurationService configurationService;
private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap();
private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap();
private final GatewayProperties gatewayProperties;
public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator, List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> gatewayFilterFactories, GatewayProperties gatewayProperties, ConfigurationService configurationService) {
this.routeDefinitionLocator = routeDefinitionLocator;
this.configurationService = configurationService;
this.initFactories(predicates);
gatewayFilterFactories.forEach((factory) -> {
GatewayFilterFactory var10000 = (GatewayFilterFactory)this.gatewayFilterFactories.put(factory.name(), factory);
});
this.gatewayProperties = gatewayProperties;
}
public Flux<Route> getRoutes() {
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
if (!this.gatewayProperties.isFailOnRouteDefinitionError()) {
routes = routes.onErrorContinue((error, obj) -> {
if (this.logger.isWarnEnabled()) {
this.logger.warn("RouteDefinition id " + ((RouteDefinition)obj).getId() + " will be ignored. Definition has invalid configs, " + error.getMessage());
}
});
}
return routes.map((route) -> {
if (this.logger.isDebugEnabled()) {
this.logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
}
private Route convertToRoute(RouteDefinition routeDefinition) {
AsyncPredicate<ServerWebExchange> predicate = this.combinePredicates(routeDefinition);
List<GatewayFilter> gatewayFilters = this.getFilters(routeDefinition);
return ((AsyncBuilder)Route.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters)).build();
}
}
通过以上部分截取源代码可以发现在父接口中定义的 getRoutes() 方法实现中将 RouteDefinition 对象转换为了 Route对象。
提问:RouteDefinition 与 Route 对象哪个才是我们关心的?,有什么差异
RouteDefinition :
package org.springframework.cloud.gateway.route;
@Validated
public class RouteDefinition {
private String id;
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList();
@Valid
private List<FilterDefinition> filters = new ArrayList();
@NotNull
private URI uri;
private Map<String, Object> metadata = new HashMap();
private int order = 0;
public RouteDefinition() {
}
}
Route :
package org.springframework.cloud.gateway.route;
public class Route implements Ordered {
private final String id;
private final URI uri;
private final int order;
private final AsyncPredicate<ServerWebExchange> predicate;
private final List<GatewayFilter> gatewayFilters;
private final Map<String, Object> metadata;
private Route(String id, URI uri, int order, AsyncPredicate<ServerWebExchange> predicate, List<GatewayFilter> gatewayFilters, Map<String, Object> metadata) {
this.id = id;
this.uri = uri;
this.order = order;
this.predicate = predicate;
this.gatewayFilters = gatewayFilters;
this.metadata = metadata;
}
}
application.yml
spring:
application:
name: microGateway
cloud:
gateway: # 网关
routes:
- id: netty-yzhenb
uri: https://www.baicu.com
predicates:
- Path=/yzhenb
- id: provider-dept
uri: lb://provider-dept # 匹配的路径
predicates:
- Path=/yzb #匹配全部的子路径
filters:
- AddRequestHeader=yzhenb1228,你好-gateway
- Log=hello,yzhenb
不仔细观察 Route 以及 RouteDefinition 结构很难发现他们之间的差异,结合application.yml就显而易见,RouteDefinition 对象是直接与application.yml挂钩的属性类,而在Gateway的内部路由通过Route存储
代码控制路由
在Gateway中存在一个实例 RouteDefinitionWriter 结合事件发布器可以完成路由的保存和删除操作,以下是他的接口定义
public interface RouteDefinitionWriter {
Mono<Void> save(Mono<RouteDefinition> route);
Mono<Void> delete(Mono<String> routeId);
}
下面是动态路由的核心代码
RouteService:
package com.yzb.gateway.service;
/**
* @author ZhenBang-Yi
* @ClassName RouteService
* @date 2022/7/20 22:13
*/
@Slf4j
@Service
public class RouteService implements ApplicationEventPublisherAware {
@Autowired
private RouteLocator routeLocator;
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher eventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}
/**
* 新增路由
* @author: ZhenBang-Yi
* @date 2022/8/2 22:10
**/
public boolean insertRoute(RouteDefinition routeDefinition) {
log.info("开始执行新增路由,ID为:{}", routeDefinition.getId());
try {
this.routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
} catch (Exception e) {
log.error("路由新增失败,ID为:{}", routeDefinition.getId());
return false;
}
return true;
}
/**
* 删除路由
* @author: ZhenBang-Yi
* @date 2022/8/2 22:09
**/
public void deleteRoute(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
log.info("成功删除路由,路由ID为:{}",id);
}catch (Exception e){
log.error("删除路由失败,未找到路由,id为:{}",id);
}
}
/**
* 路由修改
* @author: ZhenBang-Yi
* @date 2022/8/2 22:09
**/
public boolean updateRoute(RouteDefinition routeDefinition) {
try {
this.deleteRoute(routeDefinition.getId());
this.routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
this.eventPublisher.publishEvent(new RefreshRoutesEvent(this));
} catch (Exception e) {
e.printStackTrace();
log.error("更新路由失败,ID为:{}", routeDefinition.getId());
return false;
}
log.info("更新路由成功,ID为:{}",routeDefinition.getId());
return true;
}
/**
* 用户获取当前网关中所有的路由ID
* @author: ZhenBang-Yi
* @date 2022/8/2 22:09
**/
public ArrayList<List<String>> getRoutes(){
Flux<Route> routes = this.routeLocator.getRoutes();
Mono<List<String>> idList = routes.map((e) -> e.getId()).collectList();
ArrayList<List<String>> strings = new ArrayList<>();
idList.subscribe((e)->{
log.info("读取到ID有:{}",e);
strings.add(e);
});
return strings;
}
}
具体的路由业务代码编写完后,为了方便对业务代码测试所以编写一个Action方便Postman进行请求发送。
package com.yzb.gateway.action;
import com.yzb.gateway.service.RouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.web.bind.annotation.*;
/**
* @author ZhenBang-Yi
* @ClassName DynicRouteAction
* @date 2022/7/24 12:47
*/
@RequestMapping("routes/*")
@RestController
public class DynicRouteAction {
@Autowired
private RouteService routeService;
@PostMapping("/add")
public void add(@RequestBody RouteDefinition definition) {
this.routeService.insertRoute(definition);
}
@PostMapping("/delete/{id}")
public void delete(@PathVariable("id") String id) {
this.routeService.deleteRoute(id);
}
@PostMapping("/update")
public Object update(@RequestBody RouteDefinition definition) {
return this.routeService.updateRoute(definition);
}
@GetMapping("/getRoutes")
public Object getRoutes() {
return this.routeService.getRoutes();
}
}
接口测试:( PS: 通过Actuator观察所有的路由情况 )
新增路由:
删除路由:
修改路由:
结合Nacos实现动态路由
通过监听Nacos的配置文件,可以实现动态路由的加载,需要注意在Nacos的领域模型中,不同组的数据是不共享的哦~!
编写一个实体类,从配置文件中读取nacos配置信息,方便后续监听操作。
package com.yzb.gateway.entity;
import com.alibaba.nacos.api.PropertyKeyConst;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Properties;
/**
* @author ZhenBang-Yi
* @ClassName DynicRouteProperties
* @date 2022/7/31 20:05
*/
@Data
@Component
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery")
public class DynicRouteProperties {
private String dataId;
private String namespace;
private String clusterName;
private String group;
private String serverAddr;
private String service;
private String userName;
private String password;
private String dateId;
private long timeOut;
public Properties getProperties() {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, getServerAddr());
properties.put(PropertyKeyConst.NAMESPACE, getNamespace());
properties.put(PropertyKeyConst.USERNAME, getUserName());
properties.put(PropertyKeyConst.PASSWORD, getPassword());
properties.put(PropertyKeyConst.CLUSTER_NAME, getPassword());
return properties;
}
}
通过 CommandLineRunner 在程序启动后,直接读取路由的配置文件,将其转换为 RouteDefinition
package com.yzb.gateway.listening;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yzb.gateway.entity.DynicRouteProperties;
import com.yzb.gateway.service.RouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/**
* @author ZhenBang-Yi
* @ClassName DynicNacosRouteConfig
* @date 2022/7/31 20:04
*/
@Slf4j
@Component
@Configuration
public class DynicNacosRouteConfig implements CommandLineRunner {
@Autowired
private DynicRouteProperties dynicRouteProperties;
@Autowired
private RouteService routeService;
@Autowired
private ObjectMapper objectMapper;
@Bean
public void listeningNacosConfig() throws NacosException {
Properties properties = this.dynicRouteProperties.getProperties();
ConfigService configService = NacosFactory.createConfigService(properties);
// 获取数据
String config = configService.getConfig(this.dynicRouteProperties.getDataId(), this.dynicRouteProperties.getGroup(), this.dynicRouteProperties.getTimeOut());
log.info("获取到Gateway的配置数据:{}", config);
// 初始化加载路由
this.routesUpdation(config);
// 监听nacos配置文件的变化
configService.addListener(this.dynicRouteProperties.getDataId(), this.dynicRouteProperties.getGroup(), new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String content) {
log.info("读取到更新的路由数据,{}", content);
DynicNacosRouteConfig.this.routesUpdation(content);
}
});
}
@Override
public void run(String... args) throws Exception {
this.listeningNacosConfig();
}
/**
* 根据gateway配置的额数据,更新理由
*
* @param content 接受到的gateway配置数据
* @author: ZhenBang-Yi
* @date 2022/7/31 20:27
**/
public boolean routesUpdation(String content) {
try {
RouteDefinition[] routeDefinitions = this.objectMapper.readValue(content, RouteDefinition[].class);
// 获取存在当前项目中所有的路由ID
ArrayList<List<String>> routes = this.routeService.getRoutes();
List<String> collect = routes.stream().flatMap(e -> e.stream()).collect(Collectors.toList());
List<String> noneMatchIdList = collect.stream().filter((e) -> Arrays.stream(routeDefinitions).noneMatch((t) -> Objects.equals(t.getId(), e))).collect(Collectors.toList());
for (String id : noneMatchIdList) {
try {
/**
* 以配置文件为主,将多余的路由先删除
*/
this.routeService.deleteRoute(id);
} catch (Exception e) {
e.printStackTrace();
}
}
for (RouteDefinition routeDefinition : routeDefinitions) {
try {
this.routeService.updateRoute(routeDefinition);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (JsonProcessingException e) {
log.error("Nacos配置文件格式有误,不能将配置的路由结构转为 RouteDefinition.{}", e.getMessage());
e.printStackTrace();
return false;
}
return true;
}
}
启动程序,观察启动日志:
接下来访问 /provider/dept/list
更新路由数据,观察日志
此时 list 访问不了 能访问 get