SpringCloud GateWay动态路由用法
作者:Xiao_zuo_ya
1.网关为什么需要动态路由?
网关的核心功能就是通过配置不同路由策略在配合注册中心访问不同的微服务,而默认是在yaml文件中配置路由策略,而在项目上线后,网关作为所有项目的入口肯定不希望重启,所以动态路由是必须的,我们在增加一个服务,在不希望服务重新启动的前提下能路由到该服务,以及是基于代码实现的网关动态路由
2.动态路由原理
public interface RouteDefinitionRepository extends RouteDefinitionLocator, RouteDefinitionWriter { }
RouteDefinitionRepository是网关路由的存储接口,RouteDefinitionLocator 是获取存储中的所有路由,RouteDefinitionWriter主要操作路由的存储和删除。
public interface RouteDefinitionLocator { Flux<RouteDefinition> getRouteDefinitions(); }
public interface RouteDefinitionWriter { Mono<Void> save(Mono<RouteDefinition> route); Mono<Void> delete(Mono<String> routeId); }
而gateway中RouteDefinitionRepository接口的默认的实现是InMemoryRouteDefinitionRepository,即在内存中存储路由配置,而且在 GatewayAutoConfiguration 配置中也激活了InMemoryRouteDefinitionRepository这个Bean,代码如下。
@Bean @ConditionalOnMissingBean(RouteDefinitionRepository.class) public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() { return new InMemoryRouteDefinitionRepository(); }
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 -> { 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 中可见存储路由的是一个带同步锁的LinkedHashMap,而存储删除都是基于这个map对象操作。
3.动态路由设计以及实现
- 方案一:知道动态路由的原理以后,我们可以基于redis设计一个InRedisRouteDefinitionRepository 实现 RouteDefinitionRepository 接口即可,即网关部署多个也能动态解决路由问题
- 方案二:可以基于nacos 配置动态修改路由(理论上,待验证)nacos的配置也是可以热加载的。
@Slf4j @Configuration("redisRouteDefinition") @AllArgsConstructor public class InRedisRouteDefinitionRepository implements RouteDefinitionRepository { private RedisTemplate redisTemplate; @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(r -> { redisTemplate.opsForHash().put(DynamicRouterConstants.DYNAMIC_ROUTER_KEY_CONFIG, r.getId(), new Gson().toJson(r)); return Mono.empty(); }); } @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id -> { Object router = redisTemplate.opsForHash().get(DynamicRouterConstants.DYNAMIC_ROUTER_KEY_CONFIG, id); if (!Objects.isNull(router)) { redisTemplate.opsForHash().delete(DynamicRouterConstants.DYNAMIC_ROUTER_KEY_CONFIG, id); return Mono.empty(); } return Mono.defer(() -> Mono.error( new NotFoundException("RouteDefinition not found: " + routeId))); }); } @Override public Flux<RouteDefinition> getRouteDefinitions() { List<String> values = redisTemplate.opsForHash().values(DynamicRouterConstants.DYNAMIC_ROUTER_KEY_CONFIG); if (CollUtil.isNotEmpty(values)) { List<RouteDefinition> definitions = values.stream() .map(s -> new Gson().fromJson(s, RouteDefinition.class)) .collect(Collectors.toList()); return Flux.fromIterable(definitions); } else { return Flux.fromIterable(new ArrayList<>()); } } }
暂时在网关中提供接口实现路由的动态增加和修改Controller
@RestController @RequestMapping("/route") @AllArgsConstructor public class RouteController { private DynamicRouteService dynamicRouteService; @PostMapping public void saveRouteDefinition(@RequestBody GatewayRouteDefinition routeDefinition) { dynamicRouteService.saveRouteDefinition(routeDefinition); } @DeleteMapping("/{id}") public void deleteRouteDefinition(@PathVariable String id) { dynamicRouteService.deleteRouteDefinition(id); } @PutMapping public void update(@RequestBody GatewayRouteDefinition routeDefinition) { dynamicRouteService.updateRouteDefinition(routeDefinition); } @GetMapping public IPage<RouterConfig> getRouterConfigByPage(RouterConfigQueryParams params) { return dynamicRouteService.getRouterConfigByPage(params); } }
路由参数
@Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class GatewayRouteDefinition { /** * 路由的Id */ private String id; /** * 路由断言集合配置 */ private List<GatewayPredicateDefinition> predicates; /** * 路由过滤器集合配置 */ private List<GatewayFilterDefinition> filters; /** * 路由规则转发的目标uri */ private String uri; /** * 路由执行的顺序 */ private int order; }
@Data public class GatewayPredicateDefinition implements Serializable { /** * 断言对应的Name */ private String name; /** * 配置的断言规则 */ private Map<String, String> args = new LinkedHashMap<>(); }
@Data public class GatewayFilterDefinition implements Serializable { /** * Filter Name */ private String name; /** * 对应的路由规则 */ private Map<String, String> args = new LinkedHashMap<>(); }
业务层代码 DynamicRouteService,最主要的是注入RouteDefinitionWriter 我们自己的实现类,替换默认的配置
@Service public class DynamicRouteServiceImpl implements DynamicRouteService { @Resource(name = "redisRouteDefinition") private RouteDefinitionWriter routeDefinitionWriter; @Autowired private IRouterConfigService routerConfigService; @Autowired private ObjectMapper objectMapper; @Override public void saveRouteDefinition(GatewayRouteDefinition definition) { // 判定当前路由以及路径是否存在 LambdaQueryWrapper<RouterConfig> wrapper = Wrappers.<RouterConfig>lambdaQuery() .eq(RouterConfig::getRouterName, definition.getId()) .eq(RouterConfig::getRouterPath, definition.getUri()); List<RouterConfig> list = routerConfigService.list(wrapper); BizVerify.verify(CollUtil.isEmpty(list), "路由已经存在"); routerConfigService.save(paramsConvert(definition)); RouteDefinition routerDefinition = DynamicRouteUtils.convertToRouteDefinition(definition); routeDefinitionWriter.save(Mono.just(routerDefinition)).subscribe(); } @Override public void updateRouteDefinition(GatewayRouteDefinition routeDefinition) { routerConfigService.updateById(paramsConvert(routeDefinition)); RouteDefinition definition = DynamicRouteUtils.convertToRouteDefinition(routeDefinition); deleteRouteDefinition(definition.getId()); routeDefinitionWriter.save(Mono.just(definition)).subscribe(); } @Override public void deleteRouteDefinition(String routerId) { routerConfigService.removeById(routerId); routeDefinitionWriter .delete(Mono.just(routerId)) .then(Mono.defer(() -> Mono.just(ResponseEntity.ok().build()))) .onErrorResume((t) -> t instanceof NotFoundException, (t) -> Mono.just(ResponseEntity.notFound().build())); } private RouterConfig paramsConvert(GatewayRouteDefinition routeDefinition) { String filterJson = null; String PredicatesJson = null; try { filterJson = objectMapper.writeValueAsString(routeDefinition.getFilters()); PredicatesJson = objectMapper.writeValueAsString(routeDefinition.getPredicates()); } catch (JsonProcessingException e) { e.printStackTrace(); } return new RouterConfig() .setRouterName(routeDefinition.getId()) .setRouterPath(routeDefinition.getUri()) .setRouterOrder(routeDefinition.getOrder()) .setRouterFilters(filterJson) .setRouterPredicates(PredicatesJson); } @Override public IPage<RouterConfig> getRouterConfigByPage(RouterConfigQueryParams params) { LambdaQueryWrapper<RouterConfig> wrapper = Wrappers.<RouterConfig>lambdaQuery() .like(StrUtil.isNotEmpty(params.getRouterName()), RouterConfig::getRouterName, params.getRouterName()); return routerConfigService.page(new Page<>(params.getPageNum(), params.getPageSize()), wrapper); } }
4.网关中聚合swagger由于动态路由引发不展示的问题
聚合swagger聚合核心代码
package com.kill.core.provider; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import lombok.AllArgsConstructor; import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.cloud.gateway.support.NameUtils; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import springfox.documentation.swagger.web.SwaggerResource; import springfox.documentation.swagger.web.SwaggerResourcesProvider; import java.util.ArrayList; import java.util.List; /** * <pre> * +--------+---------+-----------+---------+ * | | * +--------+---------+-----------+---------+ * </pre> * * @author wangjian * @since 1019/11/01 11:58:32 */ @Component @Primary @AllArgsConstructor public class SwaggerResourceProvider implements SwaggerResourcesProvider { private static final String SWAGGER2URL = "/v2/api-docs"; private RouteDefinitionRepository repository; @Override public List<SwaggerResource> get() { List<SwaggerResource> resources = new ArrayList<>(); repository.getRouteDefinitions().subscribe( route -> { if (CollUtil.isNotEmpty(route.getPredicates())) { route.getPredicates().forEach( predicateDefinition -> { if (CollUtil.isNotEmpty(predicateDefinition.getArgs())) { if (StrUtil.isNotEmpty(predicateDefinition.getArgs().get("pattern"))) { resources.add( swaggerResource(route.getId(), predicateDefinition.getArgs().get("pattern").replace("/**", SWAGGER2URL))); } if (StrUtil.isNotEmpty(predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0"))) { resources.add( swaggerResource(route.getId(), predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("/**", SWAGGER2URL))); } } }); } }); return resources; } private SwaggerResource swaggerResource(String name, String location) { SwaggerResource swaggerResource = new SwaggerResource(); swaggerResource.setName(name); swaggerResource.setLocation(location); swaggerResource.setSwaggerVersion("2.0"); return swaggerResource; } }
5.测试一下
swagger中目前只有这一个路由,调用路由新增一个
再次刷新swagger,OK 已经看到新的路由了
redis中也已经看到了路由的配置
6.写在最后
不可能所有的代码拿过来就能用,每个人的理解也不尽相同,记录在这里希望能提供一个思路,能解决到自己遇到的问题,而不是希望大家看到后,说拷贝过来的东西都是垃圾,你可以看,如果没有帮助到你我也很遗憾。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。