您的位置:首页 > 编程语言 > Java开发

SpringCloud学习系列Gateway-(2)动态路由

2020-07-04 16:11 19 查看

目录

前言

源码分析

实现动态路由

 

前言

上篇入门篇,通过配置或者代码的方式,实现了路由。(详情请跳转: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()方法刷新路由信息就可以。

到这,一个动态路由的方案就出来了。

 

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: