SprringCloud Gateway动态添加路由不重启
文章目录
- 前言:
- 一、动态路由必要性
- 二、SpringCloud Gateway路由加载过程
- RouteDefinitionLocator接口
- PropertiesRouteDefinitionLocator类
- DiscoveryClientRouteDefinitionLocator
- InMemoryRouteDefinitionRepository
- CompositeRouteDefinitionLocator类
- CachingRouteDefinitionLocator类
- RouteLocator接口
- Route类
- RouteDefinitionRouteLocator类
- CachingRouteLocator类
- RouteRefreshListener类
- 三、Nacos实现动态路由
- 四、通过 Spring Boot Actuator实现动态路由
- 五、通过事件刷新机制自定义实现动态路由
- GatewayRouteEventPublisherAware类
- NacosRouteRefreshListener类
- NacosGatewayConfig类
- NacosRouteListener类
- isrm-gateway配置文件
- 初始化流程
- 监听流程
前言:
在微服务项目中,SpringCloud Gateway扮演着极为重要的角色,主要提供了路由、负载均衡、认证鉴权等功能。本文主要讲解如何实现网关的自定义动态路由配置,无需重启网关模块即可生效。
一、动态路由必要性
在微服务架构中,随着功能的迭代和上线,经常需要在网关添加路由配置。传统的做法是通过修改配置文件并重启网关服务来实现,但这种方式会导致服务中断,给用户带来不便。
例如如下配置:
spring: cloud: gateway: routes: - id: system predicates: - Path=/api/system/** filters: - StripPrefix=2 uri: lb://isrm-system-provider - id: basic predicates: - Path=/api/basic/** filters: - StripPrefix=2 uri: lb://isrm-basic-provider
该配置写在jar包同级的 isrm-gateway.yml文件中,假如现在网关模块是运行的,并且路由配置只有system模块。此时系统增加了basic模块,需要在配置文件中进行路由配置,但是配置完成之后,路由并未生效,只能重启网关模块去读取最新的配置来加载路由信息,重启过程中,整个网关模块都是用不了的,所有经过网关的请求都会失败,影响用户的体验。因此,动态添加路由而不重启服务成为了一个实际需求。
二、SpringCloud Gateway路由加载过程
SpringCloud Gateway路由加载过程
在看完上面的文章大概知道了路由相关类和接口的相关作用
- RoutePredicateFactory,断言工厂,用于创建具体的断言。
- GatewayFilterFactory,过滤器工厂,用于创建具体的过滤器。
- Predicate,断言接口。
- GatewayFilter,过滤器接口。
- RouteDefinition,路由定义对象,在yml里配置的路由规则其实就是配置它,包含一组断言工厂和过滤器工厂。
- Route, 路由对象,包含了一组断言规则列表和过滤器列表。
- RouteDefinitionLocator,用于获取一组RouteDefinition,最常见的就是从yml里配置,或者基于服务发现的默认路由配置。
- RouteLocator,用于把RouteDefinition转换成真正的Route对象。
RouteDefinitionLocator接口
主要有以下的实现类,该接口主要用来获取路由定义信息,比如上面yml配置文件的路由信息
PropertiesRouteDefinitionLocator类
public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator { private final GatewayProperties properties; public PropertiesRouteDefinitionLocator(GatewayProperties properties) { this.properties = properties; } @Override public Flux getRouteDefinitions() { return Flux.fromIterable(this.properties.getRoutes()); } }
主要获取配置文件的路由信息,其中GatewayProperties类标注着@ConfigurationProperties(“spring.cloud.gateway”)注解,可以获取配置文件中spring.cloud.gateway前缀的配置内容
DiscoveryClientRouteDefinitionLocator
spring.cloud.gateway.discovery.locator.enabled=true为ture时会装配该Bean
主要是从注册中心获取所有的服务列表,然后挨个加入Path断言以及去掉第一段路径的过滤器;比如某个服务名称为isrm-basic-provider,那么它会被该类发现,并加入Path断言’/isrm-basic-provider/**‘和过滤器’‘/isrm-basic-provider/(?. *)’,当访问/isrm-basic-provider/user/get时会被拦截,有过滤器重写路径为/user/get,最终访问lb://isrm-basic-provider/user/get。
如图,我并没有在配置文件配置相关路由信息,也可以经过网关访问basic服务的接口,因此可以通过DiscoveryClientRouteDefinitionLocator类发现注册的服务,然后添加默认的路由定义信息
InMemoryRouteDefinitionRepository
主要提供了对路由定义信息的增加、删除、查询方法,由一个LinkedHashMap变量存储,并包装成线程安全的SynchronizedMap
public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository { private final Map routes = synchronizedMap( new LinkedHashMap()); @Override public Mono save(Mono route) { return route.flatMap(r -> { if (StringUtils.isEmpty(r.getId())) { return Mono.error(new IllegalArgumentException("id may not be empty")); } routes.put(r.getId(), r); return Mono.empty(); }); } @Override public Mono delete(Mono routeId) { return routeId.flatMap(id -> { if (routes.containsKey(id)) { routes.remove(id); return Mono.empty(); } return Mono.defer(() -> Mono.error( new NotFoundException("RouteDefinition not found: " + routeId))); }); } @Override public Flux getRouteDefinitions() { return Flux.fromIterable(routes.values()); } }
CompositeRouteDefinitionLocator类
把其它的RouteDefinitionLocator组合在一起,也是Spring Cloud Gateway默认装配的RouteDefinitionLocator bean。加了@Primary注解会优先注入。
// GatewayAutoConfiguration类部分代码,通过入参List routeDefinitionLocators会把除了CachingRouteDefinitionLocator类 // 的所有RouteDefinitionLocator的实现类注入进来 @Bean @Primary public RouteDefinitionLocator routeDefinitionLocator(List routeDefinitionLocators) { return new CompositeRouteDefinitionLocator( Flux.fromIterable(routeDefinitionLocators)); }
CompositeRouteDefinitionLocator代码
public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator { private static final Log log = LogFactory .getLog(CompositeRouteDefinitionLocator.class); private final Flux delegates; private final IdGenerator idGenerator; public CompositeRouteDefinitionLocator(Flux delegates) { this(delegates, new AlternativeJdkIdGenerator()); } public CompositeRouteDefinitionLocator(Flux delegates, IdGenerator idGenerator) { this.delegates = delegates; this.idGenerator = idGenerator; } /** * 主要逻辑就是遍历所有注入的RouteDefinitionLocator的实现类,执行它们的getRouteDefinitions方法,合并它们返回的路由定义信息,如果路由定义信息没有 * id,则默认生成一个随机id * PropertiesRouteDefinitionLocator:获取配置文件的路由定义信息 * DiscoveryClientRouteDefinitionLocator:按服务名称生成默认的路由定义信息 * InMemoryRouteDefinitionRepository:获取维护的路由定义信息,后续SpringBoot Actuator实现动态路由以及事件刷新机制实现动态路由都是基于该类的方法 * 实现的 */ @Override public Flux getRouteDefinitions() { return this.delegates.flatMap(RouteDefinitionLocator::getRouteDefinitions) .flatMap(routeDefinition -> { if (routeDefinition.getId() == null) { return randomId().map(id -> { routeDefinition.setId(id); if (log.isDebugEnabled()) { log.debug( "Id set on route definition: " + routeDefinition); } return routeDefinition; }); } return Mono.just(routeDefinition); }); } protected Mono randomId() { return Mono.fromSupplier(idGenerator::toString) .publishOn(Schedulers.boundedElastic()); } }
CachingRouteDefinitionLocator类
该类实现了ApplicationListener接口。主要逻辑是调用CompositeRouteDefinitionLocator类的getRouteDefinitions方法,因为CompositeRouteDefinitionLocator持有了一个RouteDefinitionLocator接口的实现类列表,所以调用getRouteDefinitions方法时,会依次调用它们的getRouteDefinitions方法,并将结果合并之后,缓存到CachingRouteDefinitionLocator类的routeDefinitions和cache中,这样就不需要每次都去调用fetch方法获取路由定义信息,只有监听到RefreshRoutesEvent事件时,才会重新调用fetch方法获取最新的路由定义信息。
注意:调试代码查看流程的时候,好像该类并没有被注入进来,而RefreshRoutesEvent事件会被下面介绍的CachingRouteLocator类处理
public class CachingRouteDefinitionLocator implements RouteDefinitionLocator, ApplicationListener { private static final String CACHE_KEY = "routeDefs"; private final RouteDefinitionLocator delegate; private final Flux routeDefinitions; private final Map cache = new ConcurrentHashMap(); // 构造方法会注入RouteDefinitionLocator接口的实现类,因为CompositeRouteDefinitionLocator加了@Primary注解,所以会注入该类。 public CachingRouteDefinitionLocator(RouteDefinitionLocator delegate) { this.delegate = delegate; // 执行fetch方法,获取所有路由定义信息,缓存起来 routeDefinitions = CacheFlux.lookup(cache, CACHE_KEY, RouteDefinition.class) .onCacheMissResume(this::fetch); } // 执行CompositeRouteDefinitionLocator类的getRouteDefinitions方法 private Flux fetch() { return this.delegate.getRouteDefinitions(); } // 获取所有缓存的路由信息 @Override public Flux getRouteDefinitions() { return this.routeDefinitions; } /** * Clears the cache of routeDefinitions. * @return routeDefinitions flux */ public Flux refresh() { this.cache.clear(); return this.routeDefinitions; } // 监听RefreshRoutesEvent事件,调用fetch方法,获取最新的路由定义信息 @Override public void onApplicationEvent(RefreshRoutesEvent event) { fetch().materialize().collect(Collectors.toList()) .doOnNext(routes -> cache.put(CACHE_KEY, routes)).subscribe(); } @Deprecated /* for testing */ void handleRefresh() { refresh(); } }
RouteLocator接口
主要有以下实现类,该接口主要用于把RouteDefinition路由定义信息对象转换成真实的Route路由对象。
Route类
部分代码
public class Route implements Ordered { private final String id; private final URI uri; private final int order; private final AsyncPredicate predicate; private final List gatewayFilters; private final Map metadata; }
作用:
- 基本构建块:Route是Gateway网关的基本构建块,它负责定义和处理进入网关的网络请求。
- 组成元素:
- ID:每个Route都有一个唯一的ID,用于标识和区分不同的路由规则。
- 目标URI:目标URI指定了当路由匹配成功后,请求应该被转发到的目标地址或服务。
- 断言(Predicate)集合:断言是路由处理的第一个环节,它是一个集合,可以包含多个断言规则。这些断言规则用于匹配HTTP请求的不同属性,只有当所有断言都匹配成功时,才认为该请求匹配了当前路由。
- 过滤器(Filter)集合:如果请求通过了断言匹配,那么它将被发送到过滤器集合进行处理。过滤器可以对请求进行一系列的操作,如权限验证、参数修改等。过滤器可以在请求被转发之前或之后执行,提供了对请求和响应的精细化控制。
- 路由匹配:当客户端向Gateway发出请求时,Gateway会根据定义的Route对象进行路由匹配。如果请求与某个Route的断言集合匹配成功,那么该请求将被转发到该Route指定的目标URI,并经过该Route的过滤器集合处理。
- 服务发现和负载均衡:如果目标URI是基于服务注册名的方式(如Eureka中注册的服务名称),那么Gateway会借助服务发现机制(如Ribbon)来实现负载均衡,将请求分发到合适的服务实例上执行。
Route对象在Gateway中起到了定义路由规则、匹配网络请求、处理请求和响应的重要作用。通过配置合适的Route对象,可以实现复杂的路由逻辑和精细化的控制策略,提高系统的可扩展性和可维护性。
RouteDefinitionRouteLocator类
主要将RouteDefinition路由定义信息对象转换成真实的Route路由对象
GatewayAutoConfiguration部分代码
// 在创建RouteDefinitionRouteLocator的Bean时,会注入相关过滤器工厂、断言工厂、配置类、CompositeRouteDefinitionLocator对象 @Bean public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties, List gatewayFilters, List predicates, RouteDefinitionLocator routeDefinitionLocator, ConfigurationService configurationService) { return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, gatewayFilters, properties, configurationService); }
RouteDefinitionRouteLocator部分代码
public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAware, ApplicationEventPublisherAware { private final RouteDefinitionLocator routeDefinitionLocator; private final ConfigurationService configurationService; private final Map predicates = new LinkedHashMap(); private final Map gatewayFilterFactories = new HashMap(); private final GatewayProperties gatewayProperties; /** * 将容器中的断言工厂,过滤器工厂放到Map中,key为工厂名称前缀 * 如PathRoutePredicateFactory, 则key=Path * 这些断言工厂和过滤器工厂基本都在GatewayAutoConfiguration自动配置类注册到Spring容器中的 */ public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator, List predicates, List gatewayFilterFactories, GatewayProperties gatewayProperties, ConfigurationService configurationService) { this.routeDefinitionLocator = routeDefinitionLocator; this.configurationService = configurationService; initFactories(predicates); gatewayFilterFactories.forEach( factory -> this.gatewayFilterFactories.put(factory.name(), factory)); this.gatewayProperties = gatewayProperties; } /** * 调用CompositeRouteDefinitionLocator对象的getRouteDefinitions方法,获取所有路由定义信息,然后遍历路由定义信息列表,调用断言工厂以及过滤器工厂 * 的相关方法将断言定义信息和过滤器定义信息转成断言和过滤器,接着生成一个路由对象,添加到routes中,最后返回所有路由对象 */ @Override public Flux getRoutes() { Flux routes = this.routeDefinitionLocator.getRouteDefinitions() .map(this::convertToRoute); if (!gatewayProperties.isFailOnRouteDefinitionError()) { // instead of letting error bubble up, continue routes = routes.onErrorContinue((error, obj) -> { if (logger.isWarnEnabled()) { logger.warn("RouteDefinition id " + ((RouteDefinition) obj).getId() + " will be ignored. Definition has invalid configs, " + error.getMessage()); } }); } return routes.map(route -> { if (logger.isDebugEnabled()) { logger.debug("RouteDefinition matched: " + route.getId()); } return route; }); } }
CachingRouteLocator类
主要作用是缓存路由对象信息,不然每次请求都会生成新的路由对象信息
GatewayAutoConfiguration部分代码
/**在创建CachingRouteLocator类的Bean时,会创建CompositeRouteLocator对象,而CompositeRouteLocator对象又会持有参数中注入的 * RouteDefinitionRouteLocator的Bean */ @Bean @Primary @ConditionalOnMissingBean(name = "cachedCompositeRouteLocator") // TODO: property to disable composite? public RouteLocator cachedCompositeRouteLocator(List routeLocators) { return new CachingRouteLocator( new CompositeRouteLocator(Flux.fromIterable(routeLocators))); }
CompositeRouteLocator代码
public class CompositeRouteLocator implements RouteLocator { private final Flux delegates; public CompositeRouteLocator(Flux delegates) { this.delegates = delegates; } // 调用RouteDefinitionRouteLocator的getRoutes方法获取路由对象信息 @Override public Flux getRoutes() { return this.delegates.flatMap(RouteLocator::getRoutes); } }
CachingRouteLocator代码
public class CachingRouteLocator implements Ordered, RouteLocator, ApplicationListener { private static final String CACHE_KEY = "routes"; private final RouteLocator delegate; private final Flux routes; // 缓存路由信息 private final Map cache = new ConcurrentHashMap(); public CachingRouteLocator(RouteLocator delegate) { this.delegate = delegate; routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class) .onCacheMissResume(this::fetch); } // 调用CompositeRouteLocator的方法获取路由对象信息,每次调用都会生成新的路由信息 private Flux fetch() { return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE); } // 获取路由信息 @Override public Flux getRoutes() { return this.routes; } /** * Clears the routes cache. * @return routes flux */ public Flux refresh() { this.cache.clear(); return this.routes; } // 监听RefreshRoutesEvent事件,调用fetch方法生成新的路由信息,同时放入缓存中 @Override public void onApplicationEvent(RefreshRoutesEvent event) { fetch().materialize().collect(Collectors.toList()) .doOnNext(routes -> cache.put(CACHE_KEY, routes)).subscribe(); } @Deprecated /* for testing */ void handleRefresh() { refresh(); } @Override public int getOrder() { return 0; } }
因此,我们想要获取最新的路由信息,只需要发布一个RefreshRoutesEvent事件即可
RouteRefreshListener类
除了发布RefreshRoutesEvent事件可以获取最新路由信息之外,当Nacos配置中心发布新配置时,也会去重新获取路由信息
public class RouteRefreshListener implements ApplicationListener { private final ApplicationEventPublisher publisher; private HeartbeatMonitor monitor = new HeartbeatMonitor(); public RouteRefreshListener(ApplicationEventPublisher publisher) { Assert.notNull(publisher, "publisher may not be null"); this.publisher = publisher; } @Override public void onApplicationEvent(ApplicationEvent event) { /** * ContextRefreshedEvent:Spring容器初始化完成之后会发布该事件,初始化路由信息 * RefreshScopeRefreshedEvent:配置中心发生变化后@RefreshScope或@ConfigurationProperties标注的bean刷新完之后会发布该事件, * 然后PropertiesRouteDefinitionLocator会获取配置文件新的定义信息 * InstanceRegisteredEvent:服务注册会发布该事件,DiscoveryClientRouteDefinitionLocator会处理服务名称,获取默认路由定义信息 */ if (event instanceof ContextRefreshedEvent || event instanceof RefreshScopeRefreshedEvent || event instanceof InstanceRegisteredEvent) { reset(); } else if (event instanceof ParentHeartbeatEvent) { ParentHeartbeatEvent e = (ParentHeartbeatEvent) event; resetIfNeeded(e.getValue()); } else if (event instanceof HeartbeatEvent) { HeartbeatEvent e = (HeartbeatEvent) event; resetIfNeeded(e.getValue()); } } private void resetIfNeeded(Object value) { if (this.monitor.update(value)) { reset(); } } // 发布RefreshRoutesEvent,获取新的路由信息 private void reset() { this.publisher.publishEvent(new RefreshRoutesEvent(this)); } }
接下来说一下我所知道的三种动态路由实现方式
三、Nacos实现动态路由
前面讲了RouteRefreshListener这个监听器会监听RefreshScopeRefreshedEvent事件,当在Nacos修改了路由配置,点击发布按钮就会发布RefreshScopeRefreshedEvent事件,然后监听器监听到了这个事件,就会重新获取新的路由定义信息,然后再将这些路由定义信息转换成真正的路由对象保存在内存中。
例如我Nacos中的配置文件如下:
spring: cloud: gateway: routes: - id: sup predicates: - Path=/api/sup/** filters: - StripPrefix=2 uri: lb://isrm-sup-provider - id: auth predicates: - Path=/api/auth/** filters: - StripPrefix=2 uri: lb://isrm-auth-provider - id: basic predicates: - Path=/api/basic/** filters: - StripPrefix=2 uri: lb://isrm-basic-provider - id: system predicates: - Path=/api/system/** filters: - StripPrefix=2 uri: lb://isrm-system-provider
因为我上面没有配置合同模块的路由定义信息,所以我在本地访问合同模块的查询接口时,会报下面的异常信息,找不到对应的路由
'Failed to handle request [POST http://localhost:8081/api/contract/tContract/query]: 404 NOT_FOUND "No matching handler
在网关的配置文件加入合同模块的路由定义信息
此时点击发布按钮,配置中心的配置发生了变化,会发布一个RefreshScopeRefreshedEvent事件,RouteRefreshListener监听到这个事件会发布一个RefreshRoutesEvent事件
然后CachingRouteLocator类会监听RefreshRoutesEvent事件,接着调用CompositeRouteLocator类的方法
CompositeRouteLocator类接着调用RouteDefinitionRouteLocator类的方法
RouteDefinitionRouteLocator里面会调用CompositeRouteDefinitionLocator方法获取所有路由定义信息,并转换成真实的Route对象
CompositeRouteDefinitionLocator依次会调用其他RouteDefinitionLocator实现类的方法获取路由定义信息
PropertiesRouteDefinitionLocator类主要是获取配置文件定义的路由信息的,因为GatewayProperties被@ConfigurationProperties(“spring.cloud.gateway”)注解标注,所以它能获取最新的配置
刚刚加入的合同模块路由配置已经被读取到了,如下图,拿到这些信息就可以动态地去更新网关服务的路由信息了,不需要重启服务
此时我们再次访问合同模块的查询接口,可以发现我们已经可以成功访问到合同模块的接口了
四、通过 Spring Boot Actuator实现动态路由
-
利用 GatewayControllerEndpoint 端点暴露路由的 CRUD 操作接口。
-
引入pom文件
org.springframework.boot spring-boot-starter-actuator
-
在 yml配置文件中暴露所有端点
management: endpoints: web: exposure: include: "*"
-
GatewayControllerEndpoint类
@RestControllerEndpoint( id = "gateway" ) public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint { public GatewayControllerEndpoint(List globalFilters, List gatewayFilters, List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { super((RouteDefinitionLocator)null, globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, routeLocator); } // 获取全部路由信息 @GetMapping({"/routes"}) public Flux routes() { return this.routeLocator.getRoutes().map(this::serialize); } Map serialize(Route route) { HashMap r = new HashMap(); r.put("route_id", route.getId()); r.put("uri", route.getUri().toString()); r.put("order", route.getOrder()); r.put("predicate", route.getPredicate().toString()); if (!CollectionUtils.isEmpty(route.getMetadata())) { r.put("metadata", route.getMetadata()); } ArrayList filters = new ArrayList(); for(int i = 0; i { return route.getId().equals(id); }).singleOrEmpty().map(this::serialize).map(ResponseEntity::ok).switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); } }
@RestControllerEndpoint注解作用:
在Spring Cloud Gateway中,@RestControllerEndpoint 注解通常与Actuator端点一起使用,用于暴露管理或监控端点。然而@RestControllerEndpoint 本身并不是Spring Cloud Gateway特有的,而是Spring Boot Actuator提供的一个注解。
@RestControllerEndpoint 是 @Endpoint 和 @RestController 的组合,它允许你定义一个RESTful的Actuator端点。与 @Endpoint(它通常用于WebFlux或MVC的响应式或非响应式端点)不同,@RestControllerEndpoint 始终创建一个RESTful端点。
id 属性用于定义端点的唯一标识符,该标识符将用于URL路径(例如,/actuator/{id})。
-
通过 HTTP 请求(如使用 Postman)向这些接口发送请求,实现路由的添加、删除、查询等操作。
-
添加路由:actuator/gateway/routes/{id}
-
删除路由:actuator/gateway/routes/{id}
-
查询单条路由:actuator/gateway/routes/{id}
-
查询所有路由:actuator/gateway/routes
-
增删改接口主要在其父类AbstractGatewayControllerEndpoint上
public class AbstractGatewayControllerEndpoint implements ApplicationEventPublisherAware { private static final Log log = LogFactory.getLog(GatewayControllerEndpoint.class); protected RouteDefinitionLocator routeDefinitionLocator; protected List globalFilters; protected List GatewayFilters; protected List routePredicates; protected RouteDefinitionWriter routeDefinitionWriter; protected RouteLocator routeLocator; protected ApplicationEventPublisher publisher; public AbstractGatewayControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List gatewayFilters, List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { this.routeDefinitionLocator = routeDefinitionLocator; this.globalFilters = globalFilters; this.GatewayFilters = gatewayFilters; this.routePredicates = routePredicates; this.routeDefinitionWriter = routeDefinitionWriter; this.routeLocator = routeLocator; } public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } // 刷新路由配置接口 @PostMapping({"/refresh"}) public Mono refresh() { // 发布RefreshRoutesEvent事件 this.publisher.publishEvent(new RefreshRoutesEvent(this)); return Mono.empty(); } @GetMapping({"/globalfilters"}) public Mono globalfilters() { return this.getNamesToOrders(this.globalFilters); } @GetMapping({"/routefilters"}) public Mono routefilers() { return this.getNamesToOrders(this.GatewayFilters); } @GetMapping({"/routepredicates"}) public Mono routepredicates() { return this.getNamesToOrders(this.routePredicates); } private Mono getNamesToOrders(List list) { return Flux.fromIterable(list).reduce(new HashMap(), this::putItem); } private HashMap putItem(HashMap map, Object o) { Integer order = null; if (o instanceof Ordered) { order = ((Ordered)o).getOrder(); } map.put(o.toString(), order); return map; } // 新增接口 @PostMapping({"/routes/{id}"}) public Mono save(@PathVariable String id, @RequestBody RouteDefinition route) { // 新增路由定义信息 return Mono.just(route).filter(this::validateRouteDefinition).flatMap((routeDefinition) -> { return this.routeDefinitionWriter.save(Mono.just(routeDefinition).map((r) -> { r.setId(id); log.debug("Saving route: " + route); return r; })).then(Mono.defer(() -> { return Mono.just(ResponseEntity.created(URI.create("/routes/" + id)).build()); })); }).switchIfEmpty(Mono.defer(() -> { return Mono.just(ResponseEntity.badRequest().build()); })); } private boolean validateRouteDefinition(RouteDefinition routeDefinition) { boolean hasValidFilterDefinitions = routeDefinition.getFilters().stream().allMatch((filterDefinition) -> { return this.GatewayFilters.stream().anyMatch((gatewayFilterFactory) -> { return filterDefinition.getName().equals(gatewayFilterFactory.name()); }); }); boolean hasValidPredicateDefinitions = routeDefinition.getPredicates().stream().allMatch((predicateDefinition) -> { return this.routePredicates.stream().anyMatch((routePredicate) -> { return predicateDefinition.getName().equals(routePredicate.name()); }); }); log.debug("FilterDefinitions valid: " + hasValidFilterDefinitions); log.debug("PredicateDefinitions valid: " + hasValidPredicateDefinitions); return hasValidFilterDefinitions && hasValidPredicateDefinitions; } // 删除接口 @DeleteMapping({"/routes/{id}"}) public Mono delete(@PathVariable String id) { // 根据id删除路由定义信息 return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> { return Mono.just(ResponseEntity.ok().build()); })).onErrorResume((t) -> { return t instanceof NotFoundException; }, (t) -> { return Mono.just(ResponseEntity.notFound().build()); }); } @GetMapping({"/routes/{id}/combinedfilters"}) public Mono combinedfilters(@PathVariable String id) { return this.routeLocator.getRoutes().filter((route) -> { return route.getId().equals(id); }).reduce(new HashMap(), this::putItem); } }
-
需要注意的是,这种方式没有可视化界面,维护起来可能比较繁琐,因为需要手动调用接口来更新路由信息;如果网关有多个,那么每个网关都要手动调用接口来更新路由信息,非常繁琐;并且这些路由信息是保存在内存中的,一旦重启,这些路由信息就会失效。
-
当然,你也可以重写这些接口,对这些接口实现可视化管理界面,并将这些路由信息保存在数据库中,这样这些路由信息即使重启还会保存下来,不会丢失;对于多个网关都要重复调用接口,我觉得可以集成消息队列进来,这样只要发布更新路由的消息到消息队列中,再由消息队列广播到所有网关中,每个网关再根据消息进行处理即可。
没在yml文件配置暴露所有端点访问获取所有路由信息节点会报错
配置了就不会报错了
五、通过事件刷新机制自定义实现动态路由
在第三点介绍的Nacos基于yml文件的配置就已经可以实现动态路由了,但是我想要将路由配置和该文件隔离,自定义实现动态路由,这样不仅可以集中化配置管理路由信息,也意味着你可以进行更多的自定义扩展操作,这取决于你的动态路由实现逻辑,比如可以实现根据特定条件动态加载或卸载路由规则。
本文提供的例子,仅进行了路由信息的添加操作和动态刷新功能,可根据自己的需求,自定义实现其他扩展逻辑,代码如下:
GatewayRouteEventPublisherAware类
提供动态路由的基础方法,可通过获取bean操作该类的方法,该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
package com.itl.isrm.gateway.context; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @Slf4j @Service public class GatewayRouteEventPublisherAware implements ApplicationEventPublisherAware { /**注入RouteDefinitionWriter实现类InMemoryRouteDefinitionRepository,该类在上面已经介绍过:主要提供了对路由定义信息的增加、删除、查询方法,由一 * 个LinkedHashMap变量存储 */ @Autowired private RouteDefinitionWriter routeDefinitionWriter; // 注入事件发布器 @Autowired private ApplicationEventPublisher publisher; /** * 增加路由定义信息 * * @param definition 路由定义 * @return */ public String add(RouteDefinition definition) { log.info("新增路由:" + definition); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); // 添加完成之后需要发布RefreshRoutesEvent事件,通知CachingRouteLocator类处理RefreshRoutesEvent事件获取最新的路由配置 this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "success"; } /** * 更新路由定义信息 * * @param definition 路由定义 * @return */ public String update(RouteDefinition definition) { log.info("更新路由:" + definition); try { // 先根据id删除路由定义信息 this.routeDefinitionWriter.delete(Mono.just(definition.getId())); } catch (Exception e) { return "update fail,not find route routeId: " + definition.getId(); } try { routeDefinitionWriter.save(Mono.just(definition)).subscribe(); // 添加完成之后需要发布RefreshRoutesEvent事件,通知CachingRouteLocator类处理RefreshRoutesEvent事件获取最新的路由配置 this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "success"; } catch (Exception e) { return "update route fail"; } } /** * 删除路由定义信息 * * @param id 路由ID * @return */ public String delete(String id) { try { // 删除路由定义信息 this.routeDefinitionWriter.delete(Mono.just(id)); // 发布事件 this.publisher.publishEvent(new RefreshRoutesEvent(this)); return "delete success"; } catch (Exception e) { log.error(e.getMessage(), e); return "delete fail"; } } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }
NacosRouteRefreshListener类
主要作用是监听Nacos中的路由文件配置,当该配置文件的配置发生变化时会通知该类进行路由更新
package com.itl.isrm.gateway.listener; import com.alibaba.nacos.api.config.listener.Listener; import com.itl.isrm.common.util.JsonUtils; import com.itl.isrm.gateway.context.GatewayRouteEventPublisherAware; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.stereotype.Component; import java.util.List; import java.util.concurrent.Executor; /** * 动态实时刷新路由配置 */ @Slf4j @Component public class NacosRouteRefreshListener implements Listener { @Autowired private GatewayRouteEventPublisherAware gatewayRouteEventPublisherAware; public NacosRouteRefreshListener() { System.out.println("--->>> Init NacosRouteRefreshListener."); } @Override public Executor getExecutor() { return null; } /** * 获取最新的路由定义信息,然后由GatewayRouteEventPublisherAware对路由定义信息进行更新 * @param configInfo */ @Override public void receiveConfigInfo(String configInfo) { List list = JsonUtils.toList(configInfo, RouteDefinition.class); list.forEach(definition -> { gatewayRouteEventPublisherAware.update(definition); }); } }
NacosGatewayConfig类
主要作用是配置隔离的路由配置文件地址
package com.itl.isrm.gateway.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** * 自定义属性绑定值,可通过配置文件配置属性 */ @Configuration @ConfigurationProperties(prefix = "nacos", ignoreUnknownFields = true) public class NacosGatewayConfig { private String address; private String dataId; private String groupId; private Long timeout; private String nameSpace; public String getNameSpace() { return nameSpace; } public void setNameSpace(String nameSpace) { this.nameSpace = nameSpace; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getDataId() { return dataId; } public void setDataId(String dataId) { this.dataId = dataId; } public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public Long getTimeout() { return timeout; } public void setTimeout(Long timeout) { this.timeout = timeout; } }
需在本地配置文件中配置
spring: application: name: isrm-gateway nacos: address: ${NACOS_HOST:ip:8848} data-id: ${spring.application.name} group-id: isrm timeout: 6000 namespace: ${NAME_SPACE:dev}
NacosRouteListener类
package com.itl.isrm.gateway.listener; import com.alibaba.nacos.api.NacosFactory; import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.exception.NacosException; import com.itl.isrm.common.util.JsonUtils; import com.itl.isrm.gateway.config.NacosGatewayConfig; import com.itl.isrm.gateway.context.GatewayRouteEventPublisherAware; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.List; import java.util.Properties; /** * 服务启动时初始化路由配置信息 */ @Slf4j @Component public class NacosRouteListener { // 注入路由文件配置变化监听器 @Autowired private NacosRouteRefreshListener nacosRouteRefreshListener; // 注入配置类 @Autowired private NacosGatewayConfig nacosGatewayConfig; // 注入路由定义信息接口操作类 @Autowired private GatewayRouteEventPublisherAware gatewayRouteEventPublisherAware; /** * 当Bean初始化时执行,初始化路由配置 */ @PostConstruct public void loadRouteByNacosListener() { try { log.info("---->>> init nacos router data."); Properties nacosPro = new Properties(); nacosPro.put("serverAddr", nacosGatewayConfig.getAddress()); nacosPro.put("namespace", nacosGatewayConfig.getNameSpace()); //添加命名空间 ConfigService configService = NacosFactory.createConfigService(nacosPro); // 获取Nacos中命名空间为dev的isrm-gateway配置文件的路由定义信息 String configInfo = configService.getConfig(nacosGatewayConfig.getDataId(), nacosGatewayConfig.getGroupId(), nacosGatewayConfig.getTimeout()); // 新增路由 addRoute(configInfo); // 添加srm-gateway配置文件发生变化时的监听器,监听Nacos Server下发的动态路由配置 configService.addListener(nacosGatewayConfig.getDataId(), nacosGatewayConfig.getGroupId(), nacosRouteRefreshListener); } catch (NacosException e) { log.error(e.getMessage(), e); } } /** * 添加路由 * @param configInfo */ private void addRoute(String configInfo) { if (StringUtils.isBlank(configInfo)) { throw new NullPointerException("route info is null"); } // 将字符串转成RouteDefinition对象列表 List list = JsonUtils.toList(configInfo, RouteDefinition.class); // 遍历添加路由 list.forEach(definition -> { gatewayRouteEventPublisherAware.update(definition); }); } }
isrm-gateway配置文件
比如现在配置如下:
[ { "id": "auth", "order": 0, "predicates": [ { "args": { "pattern": "/api/auth/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-auth-provider" }, { "id": "system", "order": 0, "predicates": [ { "args": { "pattern": "/api/system/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-system-provider" }, { "id": "basic", "order": 0, "predicates": [ { "args": { "pattern": "/api/basic/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-basic-provider" }, { "id": "sup", "order": 0, "predicates": [ { "args": { "pattern": "/api/sup/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-sup-provider" } ]
初始化流程
启动网关服务,NacosRouteListener初始化时获取路由定义信息
遍历路由定义信息列表,调用GatewayRouteEventPublisherAware新增路由定义信息到InMemoryRouteDefinitionRepository中,同时发布RefreshRoutesEvent事件
CachingRouteLocator监听RefreshRoutesEvent事件,调用CompositeRouteLocator的getRoutes方法,然后由RouteDefinitionRouteLocator再去调用CompositeRouteDefinitionLocator的getRouteDefinitions方法获取所有的定义信息,然后转换成真实的路由对象
因为CompositeRouteDefinitionLocator持有了InMemoryRouteDefinitionRepository的引用,所以它能获取我们自定义维护的路由定义信息
到此,我们初始化路由配置完成
监听流程
因为上面的配置文件中没有合同模块的路由配置,所以调用合同模块的查询接口会报下面的错误
当我们在原有的配置文件基础上,新增合同模块的路由配置,然后点击发布按钮
[ { "id": "auth", "order": 0, "predicates": [ { "args": { "pattern": "/api/auth/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-auth-provider" }, { "id": "system", "order": 0, "predicates": [ { "args": { "pattern": "/api/system/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-system-provider" }, { "id": "basic", "order": 0, "predicates": [ { "args": { "pattern": "/api/basic/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-basic-provider" }, { "id": "sup", "order": 0, "predicates": [ { "args": { "pattern": "/api/sup/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-sup-provider" }, { "id": "contract", "order": 0, "predicates": [ { "args": { "pattern": "/api/contract/**" }, "name": "Path" } ], "filters": [ { "args": { "parts": "2" }, "name": "StripPrefix" } ], "uri": "lb://isrm-contract-provider" } ]
此时NacosRouteRefreshListener就能监听到配置文件的配置变化,重新调用GatewayRouteEventPublisherAware类的方法,重新加载新的路由,流程和初始化流程差不多就不讲了
重新调用合同模块的查询接口,发现数据出来了,接口没有报错,到此动态路由功能实现了,无需重启网关服务
-
-