SpringCloud微服务搭建(二)-- springcloud gateway以及其动态路由搭建

  • 一、基本的gateway项目搭建
  • 1、项目说明
  • 2、代码实现
  • 3、测试
  • 二、gateway进阶之动态路由配置
  • 1、代码实现
  • 2、测试
  • 三、源码


一、基本的gateway项目搭建

1、项目说明

这里我们不讲什么是springcloud gateway,网上类似的文章一大堆,我们直接上代码实践,这里需要用到三个项目,分别是eureka注册中心、gateway网关项目、consumer项目。
其中eureka注册中心不必说,管理其他注册上来的服务;
gateway网关项目负责配置路由分发,我们这里搭建的就是此项目;
consumer项目是模拟网关分发的服务,用户访问经过网关分发到达此服务。下面的代码实现中用的模块是:redis-lock-client

2、代码实现

gateway项目的pom如下:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--因为springboot2.x默认用的redis连接是lettuce,所以要需要导入一个连接池的包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

配置文件application.yml如下:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
          # 路由的id,服务名
        - id: redis-lock-client
          # lb代表从注册中心获取服务,并且以负载均衡方式转发
          uri: lb://redis-lock-client
          predicates:
            # 这里的Path配置的是对应服务的前缀,依靠前缀分发到不同的服务上
            - Path=/client/**
            # 这里的Header配置的是对应服务的header断言,需要有一个header参数名是tenant的值是test
            - Header=tenant,test
          # 加上StripPrefix=1,否则转发到对应服务时会带上前缀
          filters:
            - StripPrefix=1
  redis:
    # redis服务端口号
    port: 6379
    # redis服务地址
    host: 127.0.0.1
    # lettuce连接池配置
    lettuce:
      pool:
        # 连接池最大阻塞等待时间
        max-wait: 1000ms
        # 最大空闲数
        max-idle: 8
        # 最小空闲数
        min-idle: 1
        # 最大连接数
        max-active: 8
      # 关闭超时时间
      shutdown-timeout: 100ms
    timeout: 10000ms

server:
  port: 8561

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
      base-path: /
  endpoint:
    health:
      show-details: always

3、测试

分别启动三个项目,然后通过网关访问一个服务名是redis-lock-client的接口,接口地址是:/client/api/v1/test

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_spring


其中还要携带对应的header参数:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_网关_02


查看返回结果:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_网关_03


成功访问到指定的服务上。

这时我们思考下,像这样的,路由分发写在配置文件里真的好吗?真实环境中,正式使用肯定是要动态配置的,而不是像这样在配置文件里写死。

二、gateway进阶之动态路由配置

我们查看源码会发现,gateway网关的路由信息被封装到RouteDefinition类中,此类在org.springframework.cloud.gateway.route包下,定义如下:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_spring_04

1、代码实现

这里还是上面说的三个项目,只不过gateway项目需要做些修改,
其中配置文件修改如下:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1

spring:
  application:
    name: gateway-service
#  cloud:
#    gateway:
#      routes:
#          # 路由的id,服务名
#        - id: redis-lock-client
#          # lb代表从注册中心获取服务,并且以负载均衡方式转发
#          uri: lb://redis-lock-client
#          predicates:
#            # 这里的Path配置的是对应服务的前缀,依靠前缀分发到不同的服务上
#            - Path=/client/**
#            # 这里的Header配置的是对应服务的header断言,需要有一个header参数名是tenant的值是test
#            - Header=tenant,test
#          # 加上StripPrefix=1,否则转发到对应服务时会带上前缀
#          filters:
#            - StripPrefix=1
  redis:
    # redis服务端口号
    port: 6379
    # redis服务地址
    host: 127.0.0.1
    # lettuce连接池配置
    lettuce:
      pool:
        # 连接池最大阻塞等待时间
        max-wait: 1000ms
        # 最大空闲数
        max-idle: 8
        # 最小空闲数
        min-idle: 1
        # 最大连接数
        max-active: 8
      # 关闭超时时间
      shutdown-timeout: 100ms
    timeout: 10000ms

server:
  port: 8561

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
      base-path: /
  endpoint:
    health:
      show-details: always

去除配置路由分发的那部分。
然后根据上面说的源码中的路由模型定义下数据传输模型:

/**
 * 路由模型
 * @Date 2021/2/24 13:52
 * @Author hezhan
 */
@Data
public class GatewayRouteDefinition {

    /**
     * 路由id
     */
    private String id;

    /**
     * 路由断言集合配置
     */
    private List<GatewayPredicateDefinition> predicates;

    /**
     * 路由过滤器集合配置
     */
    private List<GatewayFilterDefinition> filters;

    /**
     * 路由规则转发的目标uri
     */
    private String uri;

    /**
     * 路由执行的顺序
     */
    private int order;
}
/**
 * 路由断言模型
 * @Date 2021/2/24 14:04
 * @Author hezhan
 */
@Data
public class GatewayPredicateDefinition {

    /**
     * 断言对应的name
     */
    private String name;

    /**
     * 配置的断言规则
     */
    private Map<String, String> args;
}
/**
 * 过滤器模型
 * @Date 2021/2/24 14:02
 * @Author hezhan
 */
@Data
public class GatewayFilterDefinition {

    /**
     * 过滤器名称
     */
    private String name;

    /**
     * 对应的路由规则
     */
    private Map<String, String> args;
}

然后编写Service类:

package com.hezhan.gateway.service;

import com.hezhan.gateway.entity.GatewayRouteDefinition;
import com.hezhan.gateway.util.AssembleUtil;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import .URISyntaxException;

/**
 * @Date 2021/2/24 14:16
 * @Author hezhan
 */
@Service
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    @Resource
    private RouteDefinitionWriter routeDefinitionWriter;

    @Resource
    private AssembleUtil assembleUtil;

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    /**
     * 增加路由
     * @param gatewayRouteDefinition 要新增的路由模型
     * @return
     */
    public String add(GatewayRouteDefinition gatewayRouteDefinition) throws URISyntaxException {
        RouteDefinition routeDefinition = assembleUtil.assembleRouteDefinition(gatewayRouteDefinition);
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }

    /**
     * 更新路由
     * @param gatewayRouteDefinition 要更新的路由模型
     * @return
     */
    public String update(GatewayRouteDefinition gatewayRouteDefinition) throws URISyntaxException {
        RouteDefinition routeDefinition = assembleUtil.assembleRouteDefinition(gatewayRouteDefinition);
        try {
            delete(routeDefinition.getId());
        } catch (Exception e){
            return "更新失败,没有找到路由id为:" + routeDefinition.getId() + "的路由";
        }
        try {
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e){
            return "更新路由失败";
        }
    }

    /**
     * 根据路由id删除路由
     * @param id 路由id
     * @return
     */
    public Mono<ResponseEntity<Object>> delete(String id){
        return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {
            return Mono.just(ResponseEntity.ok().build());
        })).onErrorResume((t) -> {
            return t instanceof NotFoundException;
        }, (t) -> {
            return Mono.just(ResponseEntity.notFound().build());
        });
    }
}
package com.hezhan.gateway.util;

import com.hezhan.gateway.entity.GatewayFilterDefinition;
import com.hezhan.gateway.entity.GatewayPredicateDefinition;
import com.hezhan.gateway.entity.GatewayRouteDefinition;
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 .URI;
import .URISyntaxException;
import java.util.ArrayList;
import java.util.List;

/**
 * @Date 2021/2/24 15:44
 * @Author hezhan
 */
@Component
public class AssembleUtil {

    /**
     * 将GatewayRouteDefinition类的对象组合为RouteDefinition类的对象
     * @param gatewayRouteDefinition
     * @return
     */
    public RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gatewayRouteDefinition) throws URISyntaxException {
        RouteDefinition routeDefinition = new RouteDefinition();
        routeDefinition.setId(gatewayRouteDefinition.getId());
        routeDefinition.setOrder(gatewayRouteDefinition.getOrder());
        routeDefinition.setUri(new URI(gatewayRouteDefinition.getUri()));
        // 设置断言
        List<PredicateDefinition> predicates = new ArrayList<>();
        List<GatewayPredicateDefinition> gatewayPredicateDefinitions = gatewayRouteDefinition.getPredicates();
        for (GatewayPredicateDefinition gatewayPredicateDefinition : gatewayPredicateDefinitions){
            PredicateDefinition predicateDefinition = new PredicateDefinition();
            predicateDefinition.setArgs(gatewayPredicateDefinition.getArgs());
            predicateDefinition.setName(gatewayPredicateDefinition.getName());
            predicates.add(predicateDefinition);
        }
        routeDefinition.setPredicates(predicates);

        // 设置过滤器
        List<FilterDefinition> filters = new ArrayList<>();
        List<GatewayFilterDefinition> gatewayFilterDefinitions = gatewayRouteDefinition.getFilters();
        for (GatewayFilterDefinition gatewayFilterDefinition : gatewayFilterDefinitions){
            FilterDefinition filterDefinition = new FilterDefinition();
            filterDefinition.setArgs(gatewayFilterDefinition.getArgs());
            filterDefinition.setName(gatewayFilterDefinition.getName());
            filters.add(filterDefinition);
        }
        routeDefinition.setFilters(filters);
        return routeDefinition;
    }
}

最后编写Controller层接口,通过这些接口实现动态路由功能:

package com.hezhan.gateway.controller;

import com.hezhan.gateway.entity.GatewayRouteDefinition;
import com.hezhan.gateway.service.DynamicRouteServiceImpl;
import com.hezhan.gateway.util.AssembleUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import .URISyntaxException;

/**
 * <p>通过接口来动态的新增、修改和删除网关路由</p>
 * <p>不过这种做法只能是网关服务是在单机构建的基础上,如果网关服务是集群部署,就不能这样去做</p>
 * <p>可以考虑另外创建一个路由管理的服务,然后同步到mysql和redis里,
 * 网关服务集群定时从redis或者mysql中拉取最新的路由信息,并更新</p>
 * @Date 2021/2/24 15:26
 * @Author hezhan
 */
@RestController
@RequestMapping("/api/route")
public class RouteController {

    @Resource
    private DynamicRouteServiceImpl dynamicRouteService;

    /**
     * 增加路由
     * @param gatewayRouteDefinition
     * @return
     */
    @PostMapping("/add")
    public String add(@RequestBody GatewayRouteDefinition gatewayRouteDefinition){
        String flag = "fail";
        try {
            flag = dynamicRouteService.add(gatewayRouteDefinition);
        } catch (Exception e){
            e.printStackTrace();
        }
        return flag;
    }

    /**
     * 更新路由
     * @param gatewayRouteDefinition
     * @return
     */
    @PutMapping("/update")
    public String update(@RequestBody GatewayRouteDefinition gatewayRouteDefinition) throws URISyntaxException {
        return dynamicRouteService.update(gatewayRouteDefinition);
    }

    @DeleteMapping("/{id}")
    public Mono<ResponseEntity<Object>> delete(@PathVariable String id){
        try {
            return dynamicRouteService.delete(id);
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}

2、测试

三个服务启动后,还是直接访问目标服务的接口:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_redis_05


springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_gateway_06


发现报了404,说明网关中还没有此路由分发规则。那么现在我们通过网关的接口添加路由分发规则:

有一点要注意,原本配置文件的断言中,header的规则,写法是:Header=tenant,test 这样的,我们看断言的源码:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_redis_07


那么添加路由的接口测试如下:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_java_08


运行如下:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_gateway_09


路由规则添加成功,那么我们再次访问目标服务,

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_redis_10


测试结果如下:

springcloudgateway 针对路由做分类A级路由做分流 B级路由做降级如何实现_gateway_11


访问成功!

三、源码

上述的所有代码,都已发布到github,源码地址