Gateway如何使用多个源来达成动态路由
一、介绍
在前面的文章,我介绍了如何从Nacos
读取json
文件来动态生成路由
随着文件的变更,同时刷新路由
但在文章的结尾,我并不满足于仅仅只在Nacos
配置动态路由,我想要在多个源上配置信息,任何一处地方修改了配置,Gateway
照样能够刷新路由。
那么如何使用多个源来达成动态路由?本篇文章使用了Nacos
的json
文件,和MySQL
数据表,两个配置源来达成动态路由
二、代码
首先,分析了上篇文章的RouteDefinitionRepository.java
接口,之前的Nacos
配置源也是实现了这个接口
主要是里面的这个方法,获取到所有的RouteDefinition
对象,每一个对象就是一个路由
1)BaseDynamicRouter
那么这样就好办了,我们先定义一个接口,BaseDynamicRouter.java
,里面有个方法获取到RouteDefinition
对象列表
package com.banmoon.route;
import org.springframework.cloud.gateway.route.RouteDefinition;
import java.util.List;
/**
* @author banmoon
* @date 2024/08/05 19:17:43
*/
@FunctionalInterface
public interface BaseDynamicRouter {
/**
* 获取所有的路由信息
*/
List<RouteDefinition> getAllRouterDefinitions();
}
实现这个接口BaseDynamicRouter.java
,代表着不同的配置源,我们这边有Nacos
和MySQL
,代码如下
package com.banmoon.route.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.nacos.api.config.listener.AbstractSharedListener;
import com.alibaba.nacos.api.exception.NacosException;
import com.banmoon.route.BaseDynamicRouter;
import com.banmoon.route.DynamicRouterRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
/**
* @author banmoon
* @date 2024/08/05 19:22:05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class NacosDynamicRouter implements BaseDynamicRouter {
private final ApplicationEventPublisher publisher;
private final NacosConfigProperties nacosConfigProperties;
private final NacosConfigManager nacosConfigManager;
/**
* Nacos DATA_ID
*/
private static final String DATA_ID = "gateway-router.json";
/**
* json序列化
*/
private static final ObjectMapper objectMapper = new ObjectMapper();
@PostConstruct
public void init() throws NacosException {
nacosConfigManager.getConfigService().addListener(DATA_ID, nacosConfigProperties.getGroup(), new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group, String configInfo) {
DynamicRouterRepository dynamicRouterRepository = SpringUtil.getBean(DynamicRouterRepository.class);
publisher.publishEvent(new RefreshRoutesEvent(dynamicRouterRepository));
}
});
}
@Override
public List<RouteDefinition> getAllRouterDefinitions() {
try {
String routeConfig = nacosConfigManager.getConfigService().getConfig(DATA_ID, nacosConfigProperties.getGroup(), 3000);
if (StrUtil.isNotBlank(routeConfig)) {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper.readValue(routeConfig, new TypeReference<List<RouteDefinition>>() {
});
}
} catch (Exception e) {
log.error("nacos获取路由配置异常", e);
}
return Collections.emptyList();
}
}
代码语言:java复制 package com.banmoon.route.impl;
import cn.hutool.json.JSONUtil;
import com.banmoon.route.BaseDynamicRouter;
import com.banmoon.service.SysGatewayRouteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author banmoon
* @date 2024/08/05 19:21:13
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DatabaseDynamicRouter implements BaseDynamicRouter {
private final SysGatewayRouteService sysGatewayRouteService;
@Override
public List<RouteDefinition> getAllRouterDefinitions() {
return sysGatewayRouteService
.list()
.stream()
.map(entity -> {
try {
List<PredicateDefinition> predicates = JSONUtil.toList(entity.getPredicates(), PredicateDefinition.class);
List<FilterDefinition> filters = JSONUtil.toList(entity.getFilters(), FilterDefinition.class);
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId(entity.getRouteId());
routeDefinition.setPredicates(predicates);
routeDefinition.setFilters(filters);
routeDefinition.setUri(new URI(entity.getUri()));
return routeDefinition;
} catch (URISyntaxException e) {
log.error("路由:{},uri:{},解析失败", entity.getRouteId(), entity.getUri());
return null;
}
}).filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
2)配置源
Nacos
的配置就不再贴出来了,和前面一篇文章是一样的,主要讲讲MySQL
这个配置源
首先,添加一张表
代码语言:sql复制 CREATE TABLE `sys_gateway_route` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`route_id` varchar(32) NOT NULL COMMENT '路由ID',
`uri` varchar(128) DEFAULT NULL COMMENT 'uri',
`predicates` varchar(1024) DEFAULT NULL COMMENT '断言',
`filters` varchar(1024) DEFAULT NULL COMMENT '过滤',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统-网关路由'
对应的实体、Mapper
package com.banmoon.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author banmoon
* @date 2024/08/05 19:43:06
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_gateway_route")
public class SysGatewayRouteEntity {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 路由ID
*/
private String routeId;
/**
* uri
*/
private String uri;
/**
* 断言
*/
private String predicates;
/**
* 过滤
*/
private String filters;
}
代码语言:java复制 package com.banmoon.mapper;
import com.banmoon.business.mybatis.BanmoonMapper;
import com.banmoon.entity.SysGatewayRouteEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* @author banmoon
* @date 2024/08/05 19:46:06
*/
@Mapper
public interface SysGatewayRouteMapper extends BanmoonMapper<SysGatewayRouteEntity> {
}
对应的service
服务层
package com.banmoon.service;
import com.banmoon.business.obj.dto.ResultData;
import com.banmoon.entity.SysGatewayRouteEntity;
import com.banmoon.obj.request.GatewayRouteAddRequest;
import com.banmoon.obj.request.GatewayRouteUpdateRequest;
import com.baomidou.mybatisplus.extension.service.IService;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author banmoon
* @date 2024/08/05 19:47:17
*/
public interface SysGatewayRouteService extends IService<SysGatewayRouteEntity> {
Mono<ResultData<Object>> add(GatewayRouteAddRequest request);
Mono<ResultData<Object>> update(GatewayRouteUpdateRequest request);
Mono<ResultData<Object>> delete(List<Integer> idList);
}
代码语言:java复制 package com.banmoon.service.impl;
import com.banmoon.business.obj.dto.ResultData;
import com.banmoon.entity.SysGatewayRouteEntity;
import com.banmoon.mapper.SysGatewayRouteMapper;
import com.banmoon.obj.request.GatewayRouteAddRequest;
import com.banmoon.obj.request.GatewayRouteUpdateRequest;
import com.banmoon.service.SysGatewayRouteService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* @author banmoon
* @date 2024/08/05 19:47:33
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysGatewayRouteServiceImpl extends ServiceImpl<SysGatewayRouteMapper, SysGatewayRouteEntity> implements SysGatewayRouteService {
private final ApplicationEventPublisher publisher;
@Override
public Mono<ResultData<Object>> add(GatewayRouteAddRequest request) {
return Mono.fromCallable(() -> {
SysGatewayRouteEntity entity = new SysGatewayRouteEntity();
BeanUtils.copyProperties(request, entity);
baseMapper.insertOnDuplicateKeyUpdate(entity);
return ResultData.success();
}).onErrorReturn(ResultData.fail());
}
@Override
public Mono<ResultData<Object>> update(GatewayRouteUpdateRequest request) {
return Mono.fromCallable(() -> {
SysGatewayRouteEntity entity = new SysGatewayRouteEntity();
BeanUtils.copyProperties(request, entity);
baseMapper.updateById(entity);
return ResultData.success();
}).onErrorReturn(ResultData.fail());
}
@Override
public Mono<ResultData<Object>> delete(List<Integer> idList) {
return Mono.fromCallable(() -> {
baseMapper.deleteBatchIds(idList);
return ResultData.success();
}).onErrorReturn(ResultData.fail());
}
}
对应的controller
package com.banmoon.controller;
import com.banmoon.business.obj.dto.ResultData;
import com.banmoon.obj.request.GatewayRouteAddRequest;
import com.banmoon.obj.request.GatewayRouteUpdateRequest;
import com.banmoon.service.SysGatewayRouteService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* @author banmoon
* @date 2024/08/01 10:02:24
*/
@Slf4j
@Api(tags = "系统-网关路由")
@Validated
@RestController
@RequestMapping("/sys/gateway/route")
@RequiredArgsConstructor
public class SysGatewayRouteController {
private final SysGatewayRouteService sysGatewayRouteService;
@ApiOperation("新增")
@PostMapping("add")
public Mono<ResultData<Object>> add(@RequestBody @Valid GatewayRouteAddRequest request) {
return sysGatewayRouteService.add(request);
}
@ApiOperation("修改")
@PutMapping("update")
public Mono<ResultData<Object>> update(@RequestBody @Valid GatewayRouteUpdateRequest request) {
return sysGatewayRouteService.update(request);
}
@ApiOperation("删除")
@DeleteMapping("delete")
public Mono<ResultData<Object>> delete(@RequestBody @NotEmpty List<@NotNull Integer> idList) {
return sysGatewayRouteService.delete(idList);
}
}
请求实体
代码语言:java复制 package com.banmoon.obj.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author banmoon
* @date 2024/08/05 19:59:46
*/
@Data
@ApiModel("新增路由-入参")
public class GatewayRouteAddRequest {
@NotBlank
@ApiModelProperty("路由ID")
private String routeId;
@NotBlank
@ApiModelProperty("uri")
private String uri;
@ApiModelProperty("断言")
private String predicates;
@ApiModelProperty("过滤")
private String filters;
}
代码语言:java复制 package com.banmoon.obj.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author banmoon
* @date 2024/08/05 19:59:46
*/
@Data
@ApiModel("修改路由-入参")
public class GatewayRouteUpdateRequest {
@ApiModelProperty("主键ID")
private Integer id;
@NotBlank
@ApiModelProperty("路由ID")
private String routeId;
@NotBlank
@ApiModelProperty("uri")
private String uri;
@ApiModelProperty("断言")
private String predicates;
@ApiModelProperty("过滤")
private String filters;
}
3)RouteDefinitionRepository
我们回来看看这个RouteDefinitionRepository.java
接口,只需要实现它,注入上面提到的BaseDynamicRouter.java
,实现getRouteDefinitions()
方法
package com.banmoon.route;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 路由动态更新实现
*
* @author banmoon
* @date 2024/08/01 10:12:22
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouterRepository implements RouteDefinitionRepository {
private final Map<String, BaseDynamicRouter> dynamicRouterMap;
/**
* 存储路由信息
*/
private static final Map<String, RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap<>());
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
return Flux.fromStream(dynamicRouterMap
.entrySet()
.stream()
.flatMap(entry -> entry.getValue().getAllRouterDefinitions().stream()))
.doOnNext(routeDefinition -> routes.put(routeDefinition.getId(), routeDefinition));
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
log.info("新增路由信息:{}", routeDefinition);
routes.put(routeDefinition.getId(), routeDefinition);
return Mono.empty();
});
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
log.info("删除路由ID:{}", id);
routes.remove(id);
return Mono.empty();
});
}
}
这样就能实现多个源来配置Gateway
的动态路由了
三、添加配置测试
在数据库插入一条路由信息,当然调用上面的接口也是可以的
代码语言:sql复制 INSERT INTO test.sys_gateway_route (id, route_id, uri, predicates, filters) VALUES (1, 'route_baidu', 'https://www.baidu.com', '[{"name":"Query","args":{"_genkey_0":"url","_genkey_1":"baidu"}}]', '[]');
打开浏览器,输入http://localhost:8080/?url=baidu
,可以看到百度的页面了
四、最后
其实,这个还是有点不太对,因为我发现每隔一段时间就发起一次请求更新路由
问题倒不是很大,但这么查询数据库总是一种消耗,后续看看还有没有更好的方法