背景
公司有个项目,有这样一个业务需求;需要根据某些业务点的具体的业务量,来新增微服务,而且只有这些特定的业务,会走这新增的微服务。gateway是一直在运行的,不可能增加一个新的服务,就更改配置重启服务,所以用到的动态路由方式。
大体的设计思路是这样的:gateway层添加新增、编辑、删除动态路由接口;另外一个门户服务,在新增或更新微服务后,就调用gateway层提供的接口,完成动态路由更新的功能。
问题及想法:
- 随之而来的是持久化问题,在调用gateway新增路由接口时,就会持久化路由到redis,但是动态路由是存在于内存中,一旦重启gateway服务,增加的动态路由就会消失;所以在gateway服务上添加了一个启动就会读取redis并生成动态路由的配置类;这样就可以保证就算是重启服务,路由也不会丢失。
- 另外一个问题是,当路由的过滤器进行修改,之前持久化redis中的却不会更新,所以决定在门户做一个重载接口,当修改过滤器或者路由配置时,可以触发重载接口,重载接口的作用是将持久化到redis的路由重新加载且放到gateway的内存中。
理解
什么动态路由
路由
gateway最主要的作用是,提供统一的入口,路由,鉴权,限流,熔断;这里的路由就是请求的转发,根据设定好的某些条件,比如断言,进行转发。
动态
动态的目的是让程序更加可以在运行的过程中兼容更多的业务场景。
项目的配置
简述
涉及到两个服务,一个是门户服务(作用是提供给运营人员管理入口--包括:管理路由、绑定路由),一个是网关服务(gateway组件,为门户服务提供:查询路由信息、添加路由、删除路由、编辑路由接口)
门户服务
提供出的接口

这几个都是比较简单的接口,其中绑定接口逻辑上稍微有些复杂,可以着重说一下。
解决问题过程
这个接口最主要的目的是生成新的路由,已知的有这么几个点需要注意:
如何配置过滤器
因为过滤器是一样的,只是路由的地址不同;需要获得过滤器的相关配置,并且放入到动态路由的对象中;有两个思路,一个是在代码中写死过滤器配置,更新过滤器的同时修改这块的代码(考虑到后期不便于项目维护,没有采用);第二种是先拿到配置文件中的过滤器配置,然后在放到生成的路由配置中(需要网关微服务提供一个查询当前路由配置的接口),采用此方式。
如何调用网关提供的接口
因为注册中心和网关微服务都是集群,不能保证集群的任何一台都是可靠的,所以需要从注册中心拿到网关微服务地址,代码如下:
private static Pattern PATTERN_URL = Pattern.compile("<homePageUrl>(.+?)</homePageUrl>");
private static final String REPLACE_VALUE = "eureka/";
/**
* 从eureka地址中解析出所有的access地址
*
* @param urlSuffix eureka存放服务信息的地址
* @return
*/
private List<String> getAccessUrlByEureka(String urlSuffix) {
log.info(LogFormat.formatMsg("AbilityGroupService.getAccessUrlByEureka", "", "req=" + JSON.toJSONString(urlSuffix)));
List<String> list = new ArrayList<>();
String[] eurekaSplit = eurekaArray.split(",");
Map<String, String> header = new HashMap<>();
for (String s : eurekaSplit) {
String eurekaUrl = s.replace(REPLACE_VALUE, "");
try {
HttpUtil.HTTPResult httpResult = HttpUtil.get(eurekaUrl + urlSuffix, null, header);
if (200 != httpResult.getCode()) {
continue;
}
String data = httpResult.getData();
if (Strings.isEmpty(data)) {
continue;
}
//解析出所有的网关微服务地址
Matcher matcher = PATTERN_URL.matcher(data);
while (matcher.find()) {
String homepage = matcher.group(1).trim();
list.add(homepage);
}
break;
} catch (Exception e) {
e.printStackTrace();
}
}
log.info(LogFormat.formatMsg("AbilityGroupService.getAccessUrlByEureka", "", "resp=" + JSON.toJSONString(list)));
return list;
}
拿出所有的网关微服务的地址之后,轮询调用这些服务(可以做一个重试机制),保证路由都可以下发到。
网关服务
提供的接口

重要的几个方法
可以看到这个控制器注入了两个实例,一个是RouteDefinitionLocator,这个具体的实现放到后面说,可以看到这里的作用是拿到服务运行时所有的路由的列表;另一个是DynamicRouteServiceImpl,这个类是我参考网上的博客写的,可以看一下,具体的实现。
DynamicRouteServiceImpl
@Resource
private RouteDefinitionWriter routeDefinitionWriter;
@Resource
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
publisher = applicationEventPublisher;
}
/**
* 动态增加路由
*
* @param gatewayRouteDefinition
*/
public void add(GatewayRouteDefinition gatewayRouteDefinition) {
RouteDefinition routeDefinition = assembleRouteDefinition(gatewayRouteDefinition);
log.info(LogFormat.formatMsg("DynamicRouteServiceImpl.add", "", "routeDefinition=" + JSON.toJSONString(routeDefinition)));
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
publisher.publishEvent(new RefreshRoutesEvent(this));
}
assembleRouteDefinition
//把传递进来的参数转换成路由对象
public RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {
RouteDefinition definition = new RouteDefinition();
definition.setId(gwdefinition.getId());
definition.setOrder(gwdefinition.getOrder());
//设置断言
List<PredicateDefinition> pdList = new ArrayList<>();
List<GatewayPredicateDefinition> gatewayPredicateDefinitionList = gwdefinition.getPredicates();
for (GatewayPredicateDefinition gpDefinition : gatewayPredicateDefinitionList) {
PredicateDefinition predicate = new PredicateDefinition();
predicate.setArgs(gpDefinition.getArgs());
predicate.setName(gpDefinition.getName());
pdList.add(predicate);
}
definition.setPredicates(pdList);
//设置过滤器
List<FilterDefinition> filters = new ArrayList();
List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();
for (GatewayFilterDefinition filterDefinition : gatewayFilters) {
FilterDefinition filter = new FilterDefinition();
filter.setName(filterDefinition.getName());
filter.setArgs(filterDefinition.getArgs());
filters.add(filter);
}
definition.setFilters(filters);
URI uri = null;
if (gwdefinition.getUri().startsWith("http")) {
uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
} else {
// uri为 lb://consumer-service 时使用下面的方法
uri = URI.create(gwdefinition.getUri());
}
definition.setUri(uri);
return definition;
}
小结
到这里,其实整个开发过程就结束了,功能都已经实现,就剩下测试和优化了;但是作为一个爱钻研的技术人,我们应该有打破砂锅问到底的精神;我们来分析一下调用的这个类上都有什么东西。
源码分析
可以看到DynamicRouteServiceImpl这个类并没有多少东西,主要是调用的RouteDefinitionWriter提供的两个方法save和delete,我们就可以跟着到这个类的包下,先看看类之间的关系。
UML关系图
包名:org.springframework.cloud.gateway.route
