转载

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

1.前言

本章将介绍OCP开源项目:Spring Cloud Gateway模块中动态路由的实现。

2. Spring Cloud Gateway

Spring Cloud Gateway旨在提供一种简单而有效的方式来路由到API,并为他们提供横切关注点,例如:安全性,监控/指标和弹性。

2.1 Spring Cloud Gateway特征

  • 基于Spring Framework 5,Project Reactor和Spring Boot 2.0构建
  • 能够匹配任何请求属性上的路由。
  • 谓词和过滤器特定于路线。
  • Hystrix断路器集成。
  • Spring Cloud DiscoveryClient集成
  • 易于编写谓词和过滤器
  • 请求率限制
  • 路径重写

2.2 项目实战

接下来,开始我们的 Spring Cloud Gateway 限流之旅吧!

2.2.1 Spring Cloud Gateway 限流

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

2.2.2 OCP子项目new-api-gateway

pom.xml

​
<!--基于 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>
​
复制代码

2.2.3 配置HostName的规则限流

目前只对 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
​
​
复制代码

2.2.4 新增配置类RequestRateLimiterConfig

配置类新建在 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"));
    }
}
​
复制代码

2.2.5 压力测试

接下来配置东西弄完之后 我们开始进行压力测试,压力测试之前,由于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);
}
​
​
复制代码
  • 1.打开postman 选择Collections 点击新建按钮
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
  • 2.点击新建的Collection 新建一个request

    OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
    OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
    1. 输入地址:127.0.0.1:9200/api-user/users-anon/login?username=admin
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
  • 4.切换成Tests的tab下,选择右边 Status code is 200 选项,这里可以选择其他的方法,根据自己的api定义。
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
  • 5.这里需要注意:操作完刚刚的数据,必须点击save进行保存,否则无法生效;
    OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
    请求配置
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
    1. 点击 Run,就可以查看结果,有五次成功,五次失败;限流成功返回429
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

在前一章,我们已经做了简单spring cloud gateway 介绍 和 限流,接下来,spring cloud gateway最重要的,也是最为关键的 动态路由 ,首先,API网关负责服务请求路由、组合及协议转换,客户端的所有请求都首先经过API网关,然后由它将匹配的请求路由到合适的微服务,是系统流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,如果有新的服务要上线时,可以通过动态路由配置功能上线。

3. Spring Cloud Gateway动态路由实现

首先,springcloudgateway配置路由有2种方式:

  • yml配置文件
  • 面向对象配置(代码方式配置)

3.1 yml配置

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

3.2 代码方式配置

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

3.3 路由初始化

srping cloud gateway网关启动时,路由信息默认会加载内存中,路由信息被封装到 RouteDefinition 对象中,

org.springframework.cloud.gateway.route.RouteDefinition
复制代码
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

该类有的属性为 :

@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的路由系统,所有路由信息在系统启动时就被加载装配好了,并存到了内存里。

3.4 网关的自动配置

org.springframework.cloud.gateway.config.GatewayAutoConfiguration
复制代码
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
//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
复制代码
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

RouteDefinitionLocator 类图如下:

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

子类功能描述:

  • CachingRouteDefinitionLocator:RouteDefinitionLocator包装类, 缓存目标RouteDefinitionLocator 为routeDefinitions提供缓存功能
  • CompositeRouteDefinitionLocator -RouteDefinitionLocator包装类,组合多种 RouteDefinitionLocator 的实现,为 routeDefinitions提供统一入口
  • PropertiesRouteDefinitionLocator-从配置文件(GatewayProperties 例如,YML / Properties 等 ) 读取RouteDefinition
  • RouteDefinitionRepository-从存储器( 例如,内存 / Redis / MySQL 等 )读取RouteDefinition
  • DiscoveryClientRouteDefinitionLocator-从注册中心( 例如,Eureka / Consul / Zookeeper / Etcd 等

推荐参考文章: www.jianshu.com/p/b02c7495e…

3.5 编写动态路由

新建数据脚本,在 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));
    }
​
}
​
复制代码

3.6 测试编写的动态路由

GET localhost:9200/actuator/gateway/routes
复制代码

1.使用该接口,查看gateway下的全部路由,测试路由 /jd/* * 并没有找到

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
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 都已经保存成功

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

在访问刚刚 获取全部路由的接口,发现我们的**/jd/****已经注册到我们的网关上

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
GET localhost:9200/jd/users-anon/login?username=admin
复制代码

这个时候,我们没有重启项目,依然可以访问我们自定义的路由,到此,我们已经完成了添加操作,后续的删除,更新,就是简单调用下API就完成!

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现

划重点

以上来自开源项目 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~

OCP开源项目:Spring Cloud Gateway模块中动态路由的实现
原文  https://juejin.im/post/5d748f4fe51d45621479ad53
正文到此结束
Loading...