前两篇,主要写了 实体类和表的直接操作
【SpringCloud分布式框架搭建】gateway网关,动态路由配置,使用Mysql,存储路由,实现集群gateway动态路由【二】
【SpringCloud分布式框架搭建】gateway网关,动态路由配置,使用Mysql,存储路由,实现集群gateway动态路由【一】
接下来,主要说明 gateway的动态加载的配置。
通过分析 spring Cloud gateway源码可以发现,默认的 RouteDefinitionWriter 实现类的是 InMemoryRouteDefinitionRepository ,如图:
RouteDefinitionRepository 继承了RouteDefinitionWriter,是 Spring Cloud Gateway官方预留的接口。从而可以通过下面两种方式来实现集群下的动态路由修改:RouteDefinitionWriter 接口和 RouteDefinitionRepository 接口。在这里推荐实现RouteDefinitionRepository 这个接口,从数据库或者从配置中心获取路由进行动态配置。
这里直接贴上我的mysql 实现类:MysqlRouteDefinitionRepository
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.keda.gateway.entity.DynamicRouteVo;
import com.keda.gateway.entity.GatewayDynamicRoute;
import com.keda.gateway.service.IGatewayDynamicRouteService;
import com.keda.gateway.struts.DynamicRouteStruts;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Author: seowen
* @Date: 2019/12/19 15:27
* @Version 1.0
*/
@Component
public class MysqlRouteDefinitionRepository implements RouteDefinitionRepository {
@Autowired
private IGatewayDynamicRouteService routeService;
/**
* Gateway启动的时候,会加载这个方法
* @return
*/
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
//从数据库中,获取我们自定义的 路由信息数据
List<GatewayDynamicRoute> listByEnable = routeService.getListByEnable(true);
if (CollectionUtils.isNotEmpty(listByEnable)){
//转换成 RouteDefinition 集合后,返回
return Flux.fromIterable(this.toRouteList(listByEnable));
}
//如果 数据库为 空,则返回一个 空的集合
return Flux.fromIterable(new ArrayList<RouteDefinition>());
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return null;
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return null;
}
/**
* 转换成 List<RouteDefinition>
* @param listByEnable
* @return
*/
private List<RouteDefinition> toRouteList(List<GatewayDynamicRoute> listByEnable){
List<RouteDefinition> routeList = new ArrayList<>();
/**
* 循环转换:
* 因为数据库中,Predicates 和 Filters 存储的 json字符串。所以,得先转换成 对应的 vo.
* 然后在转换成 List<PredicateDefinition>和 List<FilterDefinition>
*/
listByEnable.stream().forEach(gw->{
RouteDefinition r = new RouteDefinition();
r.setUri(DynamicUtil.getUri(gw.getUri()));
r.setOrder(gw.getOrder());
r.setId(gw.getRouteId());
r.setPredicates(DynamicRouteStruts.INSTANCES.toPredicateDefinition(JSONArray.parseArray(gw.getPredicates(), DynamicRouteVo.PredicateDefinitionVo.class)));
r.setFilters(DynamicRouteStruts.INSTANCES.toFilterDefinition(JSONArray.parseArray(gw.getFilters(),DynamicRouteVo.FilterDefinitionVo.class)));
routeList.add(r);
});
return routeList;
}
}
MysqlRouteDefinitionRepository 类写好后,还不行。 我们需要当每次操作 动态有路由表,都希望 gateway网关,重新加载 路由数据。 这个时候,我们就需要一个spring事件来支持。
DynamicRouteServiceImpl 类,就是我的 事件事件
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;
/**
* @Author: seowen
* @Date: 2019/12/19 15:49
* @Version 1.0
*/
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 增加路由
* @param routeDefinition
* @return
*/
public String add(RouteDefinition routeDefinition){
routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
this.doLoad();
return "success";
}
/**
* 更新路由
*/
public String update(RouteDefinition definition) {
try {
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();
this.doLoad();
return "success";
} catch (Exception e) {
return "update route fail";
}
}
/**
* 删除路由
*
*/
public String delete(String id) {
// return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(()->Mono.just(ResponseEntity.ok().build())))
// .onErrorResume(t -> t instanceof NotFoundException,t -> Mono.just(ResponseEntity.notFound().build()));
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.doLoad();
} catch (Exception e) {
e.printStackTrace();
return "delete fail,not find route routeId: " + id;
}
return "delete success";
}
/**
* 重新刷新 路由
*/
public String doLoad() {
try {
this.publisher.publishEvent(new RefreshRoutesEvent(this));
}catch (Exception e){
e.printStackTrace();
return "load fail";
}
return "load success";
}
}
然后,我们每次操作 数据库后,都需要调用 这个 doLoad()方法,来刷新路由:
如 GatewayDynamicRouteController 的 add方法
/**
* 增加路由
* @param routeBean
* @return
*/
@PostMapping("/add")
public String add(@RequestBody DynamicRouteVo.RouteBean routeBean){
service.saveOne(DynamicRouteStruts.INSTANCES.toGatewayDynamicRoute(routeBean));
return this.dynamicRouteService.doLoad();
}
特别说明:
{
"routeId":"keda-creditcard",
"uri":"lb://KEDA-CREDITCARD",
"order":"1",
"enable":"true",
"predicates":[
{
"name": "Path",
"args": {
"_genkey_0": "/main/**"
}
}
],
"filters":[
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
},
{
"name": "Retry",
"args": {
"retries": "3",
"series.0": "SERVER_ERROR",
"methods.0": "GET",
"methods.1": "POST"
}
}
]
}
以上,请求参数说明:
在配置 路由信息时。 一般是 name=value 的形式。
上面的 Path 和 StripPrefix 就是常用的 name属性名。特别需要注意的是,对应的值,"/main/**" 和 “1”。在gateway 静态路由配置的时候,我们会直接在 yml文件中,配置路由信息,如下图
当 predicates 和 filters 节点下,配置的是简单的 name=value 时。 gateway 在加载的时候,就会 将其按 “=”(即char 61)分割。 然后在将 value 以 “,”分割成 String 数组。然后,循环数组,以 “_genkey_” 为前缀 + 数组下标后,为key. value为value。
存到 args Map中
源码如下:
因此,我们在 添加 路由的时候,也要遵守这个的规则。如:
"name": "Path",
"args": {
"_genkey_0": "/main/**"
}
而,当添加的路由,有 args属性,并且 对应的 为 key:value 形式时,如果 key是复数(即集合类型)。
那 对应的 key 将以 “key.小标” 来表示。 如:
"name": "Retry",
"args": {
"retries": "3",
"series.0": "SERVER_ERROR",
"methods.0": "GET",
"methods.1": "POST"
}