本章将介绍OCP开源项目:Spring Cloud Gateway模块中动态路由的实现。
Spring Cloud Gateway旨在提供一种简单而有效的方式来路由到API,并为他们提供横切关注点,例如:安全性,监控/指标和弹性。
接下来,开始我们的 Spring Cloud Gateway 限流之旅吧!
<!--基于 reactive stream 的redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <!--spring cloud gateway 相关依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> 复制代码
目前只对 user-center 用户中心进行限流
spring: cloud: gateway: discovery: locator: lowerCaseServiceId: true enabled: true routes: # ===================================== - id: api-eureka uri: lb://eureka-server order: 8000 predicates: - Path=/api-eureka/** filters: - StripPrefix=1 - name: Hystrix args: name : default fallbackUri: 'forward:/defaultfallback' - id: api-user uri: lb://user-center order: 8001 predicates: - Path=/api-user/** filters: - GwSwaggerHeaderFilter - StripPrefix=1 - name: Hystrix args: name : default fallbackUri: 'forward:/defaultfallback' - name: RequestRateLimiter #对应 RequestRateLimiterGatewayFilterFactory args: redis-rate-limiter.replenishRate: 1 # 令牌桶的容积 放入令牌桶的容积每次一个 redis-rate-limiter.burstCapacity: 3 # 流速 每秒 key-resolver: "#{@ipAddressKeyResolver}" # SPEL表达式去的对应的bean 复制代码
配置类新建在 com.open.capacity.client.config 路径下
package com.open.capacity.client.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; /** * 定义spring cloud gateway中的 key-resolver: "#{@ipAddressKeyResolver}" #SPEL表达式去的对应的bean * ipAddressKeyResolver 要取bean的名字 * */ @Configuration public class RequestRateLimiterConfig { /** * 根据 HostName 进行限流 * @return */ @Bean("ipAddressKeyResolver") public KeyResolver ipAddressKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } /** * 根据api接口来限流 * @return */ @Bean(name="apiKeyResolver") public KeyResolver apiKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getPath().value()); } /** * 用户限流 * 使用这种方式限流,请求路径中必须携带userId参数。 * 提供第三种方式 * @return */ @Bean("userKeyResolver") KeyResolver userKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId")); } } 复制代码
接下来配置东西弄完之后 我们开始进行压力测试,压力测试之前,由于new-api-gateway有全局拦截器 AccessFilter 的存在,如果不想进行登录就进行测试的。先把 "/api-auth/**" 的判断中的注释掉。接下来我们开用postman进行测试
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // TODO Auto-generated method stub String accessToken = extractToken(exchange.getRequest()); if(pathMatcher.match("/**/v2/api-docs/**",exchange.getRequest().getPath().value())){ return chain.filter(exchange); } if(!pathMatcher.match("/api-auth/**",exchange.getRequest().getPath().value())){ // if (accessToken == null) { // exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // return exchange.getResponse().setComplete(); // }else{ // try { // Map<String, Object> params = (Map<String, Object>) redisTemplate.opsForValue().get("token:" + accessToken) ; // if(params.isEmpty()){ // exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // return exchange.getResponse().setComplete(); // } // } catch (Exception e) { // exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // return exchange.getResponse().setComplete(); // } // } } return chain.filter(exchange); } 复制代码
2.点击新建的Collection 新建一个request
在前一章,我们已经做了简单spring cloud gateway 介绍 和 限流,接下来,spring cloud gateway最重要的,也是最为关键的 动态路由 ,首先,API网关负责服务请求路由、组合及协议转换,客户端的所有请求都首先经过API网关,然后由它将匹配的请求路由到合适的微服务,是系统流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,如果有新的服务要上线时,可以通过动态路由配置功能上线。
首先,springcloudgateway配置路由有2种方式:
srping cloud gateway网关启动时,路由信息默认会加载内存中,路由信息被封装到 RouteDefinition 对象中,
org.springframework.cloud.gateway.route.RouteDefinition 复制代码
该类有的属性为 :
@NotEmpty private String id = UUID.randomUUID().toString(); //路由断言定义 @NotEmpty @Valid private List<PredicateDefinition> predicates = new ArrayList<>(); //路由过滤定义 @Valid private List<FilterDefinition> filters = new ArrayList<>(); //对应的URI @NotNull private URI uri; private int order = 0; 复制代码
一个RouteDefinition有个唯一的ID,如果不指定,就默认是UUID,多个RouteDefinition组成了gateway的路由系统,所有路由信息在系统启动时就被加载装配好了,并存到了内存里。
org.springframework.cloud.gateway.config.GatewayAutoConfiguration 复制代码
//RouteLocatorBuilder 采用代码的方式注入路由 @Bean public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) { return new RouteLocatorBuilder(context); } //PropertiesRouteDefinitionLocator 配置文件路由定义 @Bean @ConditionalOnMissingBean public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) { return new PropertiesRouteDefinitionLocator(properties); } //InMemoryRouteDefinitionRepository 内存路由定义 @Bean @ConditionalOnMissingBean(RouteDefinitionRepository.class) public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() { return new InMemoryRouteDefinitionRepository(); } //CompositeRouteDefinitionLocator 组合多种模式,为RouteDefinition统一入口 @Bean @Primary public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) { return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators)); } @Bean public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties, List<GatewayFilterFactory> GatewayFilters, List<RoutePredicateFactory> predicates, RouteDefinitionLocator routeDefinitionLocator) { return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates, GatewayFilters, properties); } //CachingRouteLocator 为RouteDefinition提供缓存功能 @Bean @Primary //TODO: property to disable composite? public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) { return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators))); } 复制代码
装配yml文件的,它返回的是PropertiesRouteDefinitionLocator,该类继承了RouteDefinitionLocator,RouteDefinitionLocator就是路由的装载器,里面只有一个方法,就是获取路由信息的。
org.springframework.cloud.gateway.route.RouteDefinitionLocator 复制代码
RouteDefinitionLocator 类图如下:
子类功能描述:
推荐参考文章: www.jianshu.com/p/b02c7495e…
新建数据脚本,在 sql 目录下 02.oauth-center.sql
# # Structure for table "sys_gateway_routes" # DROP TABLE IF EXISTS sys_gateway_routes; CREATE TABLE sys_gateway_routes ( `id` char(32) NOT NULL COMMENT 'id', `uri` VARCHAR(100) NOT NULL COMMENT 'uri路径', `predicates` VARCHAR(1000) COMMENT '判定器', `filters` VARCHAR(1000) COMMENT '过滤器', `order` INT COMMENT '排序', `description` VARCHAR(500) COMMENT '描述', `delFlag` int(11) DEFAULT '0' COMMENT '删除标志 0 不删除 1 删除', `createTime` datetime NOT NULL, `updateTime` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4 COMMENT '服务网关路由表'; 复制代码
/** * 路由实体类 */ public class GatewayRoutes { private String id; private String uri; private String predicates; private String filters; private Integer order; private String description; private Integer delFlag; private Date createTime; private Date updateTime; //省略getter,setter } 复制代码
/** * 路由的Service类 */ @Service public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware, IDynamicRouteService { /** * 新增路由 * * @param gatewayRouteDefinition * @return */ @Override public String add(GatewayRouteDefinition gatewayRouteDefinition) { GatewayRoutes gatewayRoutes = transformToGatewayRoutes(gatewayRouteDefinition); gatewayRoutes.setDelFlag(0); gatewayRoutes.setCreateTime(new Date()); gatewayRoutes.setUpdateTime(new Date()); gatewayRoutesMapper.insertSelective(gatewayRoutes); gatewayRouteDefinition.setId(gatewayRoutes.getId()); redisTemplate.opsForValue().set(GATEWAY_ROUTES_PREFIX + gatewayRouteDefinition.getId(), JSONObject.toJSONString(gatewayRouteDefinition)); return gatewayRoutes.getId(); } /** * 修改路由 * * @param gatewayRouteDefinition * @return */ @Override public String update(GatewayRouteDefinition gatewayRouteDefinition) { GatewayRoutes gatewayRoutes = transformToGatewayRoutes(gatewayRouteDefinition); gatewayRoutes.setCreateTime(new Date()); gatewayRoutes.setUpdateTime(new Date()); gatewayRoutesMapper.updateByPrimaryKeySelective(gatewayRoutes); redisTemplate.delete(GATEWAY_ROUTES_PREFIX + gatewayRouteDefinition.getId()); redisTemplate.opsForValue().set(GATEWAY_ROUTES_PREFIX + gatewayRouteDefinition.getId(), JSONObject.toJSONString(gatewayRouteDefinition)); return gatewayRouteDefinition.getId(); } /** * 删除路由 * @param id * @return */ @Override public String delete(String id) { gatewayRoutesMapper.deleteByPrimaryKey(id); redisTemplate.delete(GATEWAY_ROUTES_PREFIX + id); return "success"; } } 复制代码
/** * 核心类 * getRouteDefinitions() 通过该方法获取到全部路由,每次有request过来请求的时候,都会往该方法过。 * */ @Component public class RedisRouteDefinitionRepository implements RouteDefinitionRepository { public static final String GATEWAY_ROUTES_PREFIX = "geteway_routes_"; @Autowired private StringRedisTemplate redisTemplate; private Set<RouteDefinition> routeDefinitions = new HashSet<>(); /** * 获取全部路由 * @return */ @Override public Flux<RouteDefinition> getRouteDefinitions() { /** * 从redis 中 获取 全部路由,因为保存在redis ,mysql 中 频繁读取mysql 有可能会带来不必要的问题 */ Set<String> gatewayKeys = redisTemplate.keys(GATEWAY_ROUTES_PREFIX + "*"); if (!CollectionUtils.isEmpty(gatewayKeys)) { List<String> gatewayRoutes = Optional.ofNullable(redisTemplate.opsForValue().multiGet(gatewayKeys)).orElse(Lists.newArrayList()); gatewayRoutes .forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition, RouteDefinition.class))); } return Flux.fromIterable(routeDefinitions); } /** * 添加路由方法 * @param route * @return */ @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(routeDefinition -> { routeDefinitions.add( routeDefinition ); return Mono.empty(); }); } /** * 删除路由 * @param routeId * @return */ @Override public Mono<Void> delete(Mono<String> routeId) { return routeId.flatMap(id -> { List<RouteDefinition> collect = routeDefinitions.stream().filter( routeDefinition -> StringUtils.equals(routeDefinition.getId(), id) ).collect(Collectors.toList()); routeDefinitions.removeAll(collect); return Mono.empty(); }); } } 复制代码
/** * 编写Rest接口 */ @RestController @RequestMapping("/route") public class RouteController { @Autowired private IDynamicRouteService dynamicRouteService; //增加路由 @PostMapping("/add") public Result add(@RequestBody GatewayRouteDefinition gatewayRouteDefinition) { return Result.succeed(dynamicRouteService.add(gatewayRouteDefinition)); } //更新路由 @PostMapping("/update") public Result update(@RequestBody GatewayRouteDefinition gatewayRouteDefinition) { return Result.succeed(dynamicRouteService.update(gatewayRouteDefinition)); } //删除路由 @DeleteMapping("/{id}") public Result delete(@PathVariable String id) { return Result.succeed(dynamicRouteService.delete(id)); } } 复制代码
GET localhost:9200/actuator/gateway/routes 复制代码
1.使用该接口,查看gateway下的全部路由,测试路由 /jd/* * 并没有找到
POST 127.0.0.1:9200/route/add 复制代码
参数由json格式构建 对应 com.open.capacity.client.dto.GatewayRouteDefinition 类
{ "id": "", "uri": "lb://user-center", "order": 1111, "filters": [ { "name": "StripPrefix", "args": { "_genkey_0": "1" } } ], "predicates": [ { "name": "Path", "args": { "_genkey_0": "/jd/**" } } ], "description": "测试路由新增" } 复制代码
添加成功,返回对应id,查看mysql,redis 都已经保存成功
在访问刚刚 获取全部路由的接口,发现我们的**/jd/****已经注册到我们的网关上
GET localhost:9200/jd/users-anon/login?username=admin 复制代码
这个时候,我们没有重启项目,依然可以访问我们自定义的路由,到此,我们已经完成了添加操作,后续的删除,更新,就是简单调用下API就完成!
以上来自开源项目 OCP : gitee.com/owenwangwen…
项目演示地址 http://59.110.164.254:8066/login.html 用户名/密码:admin/admin
项目监控 http://106.13.3.200:3000 用户名/密码:admin/1q2w3e4r
项目代码地址 gitee.com/owenwangwen…
群号:483725710(备注:Coder编程)欢迎大家加入~
欢迎关注个人微信公众号: Coder编程 获取最新原创技术文章和免费学习资料,更有大量精品思维导图、面试资料、PMP备考资料等你来领,方便你随时随地学习技术知识!
欢迎 关注 并star~