SpringCloud学习系列Gateway-(2)动态路由
目录
前言
上篇入门篇,通过配置或者代码的方式,实现了路由。(详情请跳转:SpringCloud学习系列Gateway-(1)入门篇)
但这种配置方式有个弊端,就是每次接入一个新应用或者变更应用访问路径,就需要重新配置网关,新增或修改路由规则,然后重启gateway;对于一个网关层来说,一旦出现这种情况,就会影响所有接入应用在这段时间都不能访问,这无疑是不可行的。
那么,这一章,我们就看下如何采用动态路由的方式来解决这种问题。
源码分析
首先,我们来看下SpringCloud Gateway的初始化方式和路由执行方式。(gateway的maven版本:2.2.3.RELEASE)
1、初始化配置信息,我们来看下org.springframework.cloud:spring-cloud-gateway-core包下面的META-INF/spring.factories文件;
[code]# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ // 校验引用配置 org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\ // gateway网关的核心配置,路由等 org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\ // 熔断配置 org.springframework.cloud.gateway.config.GatewayHystrixCircuitBreakerAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration,\ // 负载均衡配置 org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\ // redis限流配置 org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\ // 注册中心配置 org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\ org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\ org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor
这一章,我们只需关注网关的核心配置类 org.springframework.cloud.gateway.config.GatewayAutoConfiguration
[code]// 这里一些其他的代码就先省略 public class GatewayAutoConfiguration { @Bean @ConditionalOnMissingBean public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator( GatewayProperties properties) { return new PropertiesRouteDefinitionLocator(properties); } @Bean @ConditionalOnMissingBean(RouteDefinitionRepository.class) public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() { return new InMemoryRouteDefinitionRepository(); } // 初始化装载不同路由配置方式的列表 @Bean @Primary public RouteDefinitionLocator routeDefinitionLocator( List<RouteDefinitionLocator> routeDefinitionLocators) { return new CompositeRouteDefinitionLocator( Flux.fromIterable(routeDefinitionLocators)); } @Bean public RouteRefreshListener routeRefreshListener( ApplicationEventPublisher publisher) { return new RouteRefreshListener(publisher); } }
这里第三个方法就是负责把继承了routeDefinitionLocator接口类的不同方式路由配置汇总;PropertiesRouteDefinitionLocator 和InMemoryRouteDefinitionRepository 都继承了 routeDefinitionLocator 接口类,对应第一个方法和第二个方法的初始化类。
[code]public class PropertiesRouteDefinitionLocator implements RouteDefinitionLocator { private final GatewayProperties properties; public PropertiesRouteDefinitionLocator(GatewayProperties properties) { this.properties = properties; } @Override public Flux<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable(this.properties.getRoutes()); } }
PropertiesRouteDefinitionLocator 就是加载application.properties或者application.yml文件中配置的路由信息;
[code]public class InMemoryRouteDefinitionRepository implements RouteDefinitionRepository { private final Map<String, RouteDefinition> routes = synchronizedMap( new LinkedHashMap<String, RouteDefinition>()); @Override public Mono<Void> save(Mono<RouteDefinition> 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<Void> delete(Mono<String> 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<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable(routes.values()); } }
InMemoryRouteDefinitionRepository 从代码中可以看到,使用Map<String, RouteDefinition> routes来存储路由信息,就是使用了本地缓存的方式。(这里RouteDefinitionRepository 继承了routeDefinitionLocator )
但是InMemoryRouteDefinitionRepository 的bean初始化方式采用了@ConditionalOnMissingBean(RouteDefinitionRepository.class),这表示如果没有其他继承了RouteDefinitionRepository.class的类进行bean初始化的前提下,就初始化InMemoryRouteDefinitionRepository。
(注意:这里就是预留的让我们进行自定义路由加载的实现方式,后面实现动态路由就可以从这里入手)
我们再来看下这些类继承的routeDefinitionLocator类需要实现的是什么。
[code]public interface RouteDefinitionLocator { Flux<RouteDefinition> getRouteDefinitions(); }
这个接口类很简单,只需要实现一个方法:获取路由信息类RouteDefinition的列表。
[code]@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; }
RouteDefinition的属性值,我们可以看下,其实是跟使用properties或者yml方式配置的一些参数书一一对应的。
那么这里就有个问题了,难道请求每次都会执行getRouteDefinitions()方法获取下路由信息吗?然而,并不会。
既然不会,怎么实现动态更新路由信息呢,我们回到GatewayAutoConfiguration配置类看第四个方法,初始化了一个监听器RouteRefreshListener。
[code]public class RouteRefreshListener implements ApplicationListener<ApplicationEvent> { @Override public void onApplicationEvent(ApplicationEvent event) { 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(); } } private void reset() { this.publisher.publishEvent(new RefreshRoutesEvent(this)); } }
监听器里面监听了一个HeartbeatEvent的心跳事件,这里会发送RefreshRoutesEvent事件进行路由缓存刷新。
因为引入了eureka注册中心,这个心跳事件的发出者是由CloudEurekaClient定时发出,(当然这里也可以我们自己发出RefreshRoutesEvent来主动刷新),代码如下:
[code]public class CloudEurekaClient extends DiscoveryClient { protected void onCacheRefreshed() { super.onCacheRefreshed(); if (this.cacheRefreshedCount != null) { long newCount = this.cacheRefreshedCount.incrementAndGet(); log.trace("onCacheRefreshed called with count: " + newCount); this.publisher.publishEvent(new HeartbeatEvent(this, newCount)); } } }
那么,到这里,相信大家都知道怎么去实现动态路由了。接下来,动手敲代码吧。
实现动态路由
1、继承RouteDefinitionRepository实现自己的获取路由信息类
[code]import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; @Component public class DynamicRouteDefinitionRepository implements RouteDefinitionRepository { @Resource private DynamicRouteConfig dynamicRouteConfig; @Override public Flux<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable(dynamicRouteConfig.getRouteDefinitions()); } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation"))); } @Override public Mono<Void> delete(Mono<String> routeId) { return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation"))); } }
2、自定义的路由操作类DynamicRouteConfig
[code]import org.springframework.cloud.gateway.filter.FilterDefinition; import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.util.*; @Component public class DynamicRouteConfig { /** * @Description 后期可以采用数据库存储方式或者其他可以变更的存储方式 **/ public List<RouteDefinition> getRouteDefinitions() { RouteDefinition definition = new RouteDefinition(); definition.setId("gateway-service"); definition.setUri(URI.create("lb://gateway-service")); //定义第一个断言 PredicateDefinition predicate = new PredicateDefinition(); predicate.setName("Path"); Map<String, String> predicateParams = new HashMap<>(4); predicateParams.put("pattern", "/service/**"); predicate.setArgs(predicateParams); //定义Filter FilterDefinition filter = new FilterDefinition(); filter.setName("RequestRateLimiter"); Map<String, String> filterParams = new HashMap<>(8); filterParams.put("key-resolver", "#{@uriKeyResolver}"); filterParams.put("redis-rate-limiter.replenishRate", "2"); filterParams.put("redis-rate-limiter.burstCapacity", "2"); filter.setArgs(filterParams); definition.setFilters(Arrays.asList(filter)); definition.setPredicates(Arrays.asList(predicate)); List<RouteDefinition> list = new ArrayList<>(4); list.add(definition); return list; } }
这里我直接采用硬编码的方式,方便测试;后期大家可以直接从数据库或者其他存储获取这些参数,进行List<RouteDefinition>的拼装,这样当心跳事件接收到时,会重新从数据库拉取最新的配置数据更新路由信息,这样就达到动态路由配置的效果。
这里也可以自己进行主动推送事件进行刷新,实现发送RefreshRoutesEvent事件
[code]import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.stereotype.Component; @Component public class DynamicRouteEventPublisher implements ApplicationEventPublisherAware { private ApplicationEventPublisher eventPublisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } public void notify() { // 重新刷新,如果多节点的情况下,可以采用MQ的广播消息方式通知,进行更新 this.eventPublisher.publishEvent(new RefreshRoutesEvent(this)); } }
这样在管理后台配置好路由信息后,保存到数据库,然后使用消息广播的方式通知各个gateway服务器实例调用这里的notify()方法刷新路由信息就可以。
到这,一个动态路由的方案就出来了。
- vue系列之动态路由【原创】
- vue系列之动态路由【原创】
- vue系列之动态路由【原创】
- Spring Cloud Gateway的动态路由怎样做?集成Nacos实现很简单
- SpringCloud学习系列Gateway-(3)限流篇
- Spring Cloud Gateway的动态路由怎样做?集成Nacos实现很简单
- Spring Cloud Gateway的动态路由怎样做?集成Nacos实现很简单
- vue系列之动态路由详解【原创】
- SpringCloud学习系列之六 ----- 路由网关Zuul基础使用教程
- Nginx技术研究系列2-基于Redis实现动态路由
- Spring Cloud 系列之 Spring cloud gateway 实现网关路由转发和过滤功能
- vue系列之动态路由【原创】
- Ngnix技术研究系列2-基于Redis实现动态路由
- vue系列之动态路由【原创】
- SpringCloud学习系列之七 ----- Zuul路由网关的过滤器和异常处理
- Spring系列之 (十一):AOP实现方式:动态代理的两个方式(JDK和Cglib)
- 【转】使用spring的动态路由实现数据库负载均衡
- CCNP精粹系列之十----路由再发布