SpringCloud学习系列Gateway-(2)动态路由
目录前言前言上篇入门篇,通过配置或者代码的方式,实现了路由。(详情请跳转:SpringCloud学习系列Gateway-(1)入门篇)
目录
前言
上篇入门篇,通过配置或者代码的方式,实现了路由。(详情请跳转:SpringCloud学习系列Gateway-(1)入门篇)
但这种配置方式有个弊端,就是每次接入一个新应用或者变更应用访问路径,就需要重新配置网关,新增或修改路由规则,然后重启gateway;对于一个网关层来说,一旦出现这种情况,就会影响所有接入应用在这段时间都不能访问,这无疑是不可行的。
那么,这一章,我们就看下如何采用动态路由的方式来解决这种问题。
源码分析
首先,我们来看下SpringCloud Gateway的初始化方式和路由执行方式。(gateway的maven版本:2.2.3.RELEASE)
1、初始化配置信息,我们来看下org.springframework.cloud:spring-cloud-gateway-core包下面的META-INF/spring.factories文件;
# 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
// 这里一些其他的代码就先省略
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 接口类,对应第一个方法和第二个方法的初始化类。
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文件中配置的路由信息;
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类需要实现的是什么。
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}
这个接口类很简单,只需要实现一个方法:获取路由信息类RouteDefinition的列表。
@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。
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来主动刷新),代码如下:
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实现自己的获取路由信息类
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
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事件
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()方法刷新路由信息就可以。
到这,一个动态路由的方案就出来了。
更多推荐
所有评论(0)