天天看点

【微服务学习】用SpringCloud Gateway做一个动态API网关

1. SpringCloud Gateway

先来了解一些概念。

1. 简介

SpringCloud Gateway

是一个建立在

Spring

生态之上,基于

Spring5

Spring Boot 2

Project Reactor

的API网关。目标是提供一个简单但是有效的方式把请求路由到API,并提供像是安全、监控/指标和弹性之类的值得关注的切面。

2. 几个术语

  • Route

    :网关的基本构件。由一个

    ID

    ,一个目标

    URI

    ,一个

    predicates

    的集合,一个

    filters

    的集合组成。当

    predicates

    总体判断是

    true

    的时候,这个

    route

    就算匹配成功了。
  • Predicate

    :这是一个Java8 Function Predicate。输入类型是一个

    Spring FrameWork ServerWebExchange

    。这允许您匹配来自HTTP请求的任何内容,比如通过请求头或者请求参数。
  • Filter

    :是由特定的工厂类构建的

    GatewayFilter

    的实例。在发送下游请求之前或者之后,你可以在其中对请求或者响应做修改。
    说明:如果在

    route

    URI

    里面没有定义端口的话,HTTP和HTTPS的端口默认分别会被解析为为80和443。

3. SpringCloud Gateway是如何工作的

下面的这张图片整体描述了

SpringCloud Gateway

是如何工作的:

【微服务学习】用SpringCloud Gateway做一个动态API网关

客户端向

SpringCloud Gateway

发送请求,如果

Gateway Handler Mapping

判定请求匹配到了一个

route

,就把它发送给

Gateway Web Handler

,它用这个请求匹配到的特定的过滤器链来处理这个请求。图中

Filter

中间虚线的意思是它在代理的请求被发送之前和之后都可以执行逻辑。也就是,先执行所有的前置过滤器逻辑,然后发送代理的请求,请求完成后,再执行后置的过滤器逻辑。

2. 依赖和配置

1. pom.xml

除了

spring-cloud-starter-gateway

本身的依赖之外,这里还引入了

spring-boot-starter-actuator

来监控路由表,引入了

hutool-all

工具包方便对URL进行解析处理。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.makabaka</groupId>
    <artifactId>apigateway</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.2.10.RELEASE</version>
    </parent>
f
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <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>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
        </dependency>
    </dependencies>
</project>
           

2. application.yaml

server:
  port: 8080
spring:
  cloud:
    gateway:
      globalcors: #跨域配置,这里全部放开,实际使用时再按照需要修改
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods: "*"
            allowedHeaders: "*"
management: #actuator配置
  endpoint:
    gateway:
      enabled: true #启用SpringCloud Gateway监控端点
  endpoints:
    web:
      exposure:
        include: gateway #暴露SpringCloud Gateway监控端点
           

3. 路由

在做路由转发之前,我们先来找一个公共api接口作为被转发的路由,比如这个。用它提供的每日一言的接口来测试,地址是:https://v.api.aa1.cn/api/yiyan/index.php。

1. 先从静态路由开始

1. Predicate

SpringCloud Gateway

内置了多达十二种

Predicate

匹配方式来对请求的不同属性进行路由匹配,而且可以混合使用,详细文档可以看这里。我们用

path route

进行路由匹配。在

application.yaml

里添加如下配置:

spring:
  cloud:
    gateway:
      routes:
        - id: one
          uri: https://v.api.aa1.cn
          predicates:
            - Path=/api/yiyan/index.php
           

启动项目,打开浏览器,访问

http://localhost:8080/api/yiyan/index.php

,可以看到一句优美的文字出现在了浏览器里,我这里看到的是

但使主人能醉客,不知何处是他乡。 ——李白

说明访问

http://localhost:8080/api/yiyan/index.php

的请求已经被转发到了

https://v.api.aa1.cn/api/yiyan/index.php

,我们的第一步成功了。

但是这里我们是直接使用了API接口的url,如果希望把请求

http://localhost:8080/api/oneword

转发到

https://v.api.aa1.cn/api/yiyan/index.php

,应该怎样配置呢?用

Filter

2. Filter

Filter

可以按照指定的规则对请求和响应进行修改,

SpringCloud Gateway

内置了三十多种

Filter

,详细文档可以看这里,使用细节可以参考官方给出的单元测试例子。这里可以用

RewritePath Filter

来重写path。修改配置文件:

spring:
  cloud:
    gateway:
      routes:
        - id: one
          uri: https://v.api.aa1.cn
          predicates:
            - Path=/api/oneword
          filters:
            - RewritePath=/api/oneword,/api/yiyan/index.php
           

重启项目,打开浏览器,访问

http://localhost:8080/api/oneword

,这次看到的是

世间无限丹青手,一片伤心画不成。——高蟾

说明访问

http://localhost:8080/api/one

的请求被我们配置的

RewritePath Filter

重写并转发到了

https://v.api.aa1.cn/api/yiyan/index.php

2. 改成动态路由

简单掌握了静态路由的配置之后,我们来尝试把路由改为动态配置,第一步先把配置方式从

yaml

配置改为使用Java代码进行配置。首先把

application.yaml

中的路由配置注释掉,然后定义一个路由的实体类。

public class LocalRoute {
    //路由id
    private String id;
    //api地址
    private String url;
    //转发的路径
    private String path;
	//省略get、set和构造方法
}
           

使用Java代码进行路由配置的关键在于创建一个类去实现

RouteDefinitionRepository

接口,并在

getRouteDefinitions

方法的实现里配置路由信息。

@Component
public class StaticRouteDefinitionRepositoryImpl implements RouteDefinitionRepository {
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {

        List<LocalRoute> localRouteList = new ArrayList<>();
        
        localRouteList.add(new LocalRoute("1", "https://v.api.aa1.cn/api/yiyan/index.php", "/oneword"));

        List<RouteDefinition> routeDefinitionList = new ArrayList<>();

        for (LocalRoute localRoute : localRouteList) {
            RouteDefinition routeDefinition = new RouteDefinition();
            PredicateDefinition predicateDefinition = new PredicateDefinition();

            //Route
            routeDefinition.setId(localRoute.getId());

            //处理api的url,拆分为uri和path两部分
            URL url = URLUtil.url(localRoute.getUrl());
            String uri = url.getProtocol() + "://" + url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
            String rewritePath = url.getPath();
            try {
                routeDefinition.setUri(new URI(uri));
            } catch (URISyntaxException e) {
                //如果URI格式不正确,跳过这个路由配置
                e.printStackTrace();
                continue;
            }

            //Predicate
            String localPath = "/api" + localRoute.getPath();//统一加上api前缀便于后续鉴权判断
            predicateDefinition.setName("Path");
            predicateDefinition.addArg("Path", localPath);
            routeDefinition.setPredicates(Collections.singletonList(predicateDefinition));

            //Filter
            List<FilterDefinition> filterDefinitionList = new ArrayList<>();

            //判断path使用通配符的情况,处理重写path配置
            if (localRoute.getPath().endsWith("/**")) {
                localPath = localPath.replace("/**", "/?(?<segment>.*)");
                rewritePath = rewritePath + "/${segment}";
            }

            //RewritePath Filter
            FilterDefinition rewritePathFilterDefinition = new FilterDefinition();
            rewritePathFilterDefinition.setName("RewritePath");
            rewritePathFilterDefinition.addArg("regexp", localPath);
            rewritePathFilterDefinition.addArg("replacement", rewritePath);

            filterDefinitionList.add(rewritePathFilterDefinition);
            routeDefinition.setFilters(filterDefinitionList);

            routeDefinitionList.add(routeDefinition);
        }
        return Flux.fromIterable(routeDefinitionList);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return null;
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return null;
    }
}
           

保存,重启。这次我们用

actuator api

来查看路由表,请求

http://localhost:8080/actuator/gateway/routes

,响应如下:

[
    {
        "predicate": "Paths: [/api/oneword], match trailing slash: true",
        "route_id": "1",
        "filters": [
            "[[RewritePath /api/oneword = '/api/yiyan/index.php'], order = 1]"
        ],
        "uri": "https://v.api.aa1.cn:443",
        "order": 0
    }
]
           

打开浏览器,访问

http://localhost:8080/api/oneword

,这次看到的是

直道相思了无益,未妨惆怅是清狂。——李商隐

说明我们使用Java代码配置的路由也生效了。

聪明的你肯定想到了,只需要把

localRouteList

改为从你需要的地方获取(配置文件、数据库、Redis等等),就可以实现动态的获取路由配置。那么只需要当路由配置发生变化时,我们能刷新路由表,动态路由配置就完成了。

3. 刷新动态路由

SpringCloud Gateway

提供了刷新路由的事件,我们在需要时把这个事件发送给

Spring

,就可以刷新路由了。

@Component
public class RefreshRouteService implements ApplicationEventPublisherAware {
    @Autowired
    ApplicationEventPublisher applicationEventPublisher;

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

    //调用这个方法就可以刷新路由了
    public void refreshRoutes() {
        System.out.println("refresh routes");
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
    }
}
           

测试一下,先增加一个

controller

提供刷新路由的接口

@RestController
@RequestMapping("/route")
public class RefreshRouteController {
    @Autowired
    private RefreshRouteService refreshRouteService;

    @GetMapping("/refresh")
    public String refresh() {
        refreshRouteService.refreshRoutes();
        return "已刷新路由表";
    }
}
           

然后把获取路由配置的方式改为从txt文件中读取,这里仅作测试用,所以写的简单粗暴一些

try {
    Scanner scanner = new Scanner(new File("C:\Users\Makabaka\Desktop\route.txt"));
    while (scanner.hasNextLine()) {
        String[] localRouteArr = scanner.nextLine().split(",");
        localRouteList.add(new LocalRoute(localRouteArr[0], localRouteArr[1], localRouteArr[2]));
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
    return null;
}
           

route.txt

的内容

1,https://v.api.aa1.cn/api/yiyan/index.php,/oneword
           

启动项目,查看路由表:

[
    {
        "predicate": "Paths: [/api/oneword], match trailing slash: true",
        "route_id": "1",
        "filters": [
            "[[RewritePath /api/oneword = '/api/yiyan/index.php'], order = 1]"
        ],
        "uri": "https://v.api.aa1.cn:443",
        "order": 0
    }
]
           

route.txt

增加一行搞笑段子的路由

1,https://v.api.aa1.cn/api/yiyan/index.php,/oneword
2,https://v.api.aa1.cn/api/api-wenan-gaoxiao/index.php,/funny
           

调用

http://localhost:8080/route/refresh

,看到返回"已刷新路由表",再来看路由表

[
    {
        "predicate": "Paths: [/api/oneword], match trailing slash: true",
        "route_id": "1",
        "filters": [
            "[[RewritePath /api/oneword = '/api/yiyan/index.php'], order = 1]"
        ],
        "uri": "https://v.api.aa1.cn:443",
        "order": 0
    },
    {
        "predicate": "Paths: [/api/funny], match trailing slash: true",
        "route_id": "2",
        "filters": [
            "[[RewritePath /api/funny = '/api/api-wenan-gaoxiao/index.php'], order = 1]"
        ],
        "uri": "https://v.api.aa1.cn:443",
        "order": 0
    }
]
           

请求

http://localhost:8080/api/funny?aa1=text

,(注意这个接口需要传参数

aa1=text

),返回结果

步步高打火机,哪里不会点哪里,妈妈以后再也不用担心我学习了。

emmm,虽然段子不好笑,但是说明转发成功了,刷新路由也成功了。

4. 功能扩展

我们基于

SpringCloud Gateway

Route

Predicate

Filter

实现了动态路由配置,刷新功能。但一个实际使用的API网关的需求可能还包括鉴权、请求记录、流量统计等等。这些都可以使用

SpringCloud Gateway

的其它特性,比如

GlobalFilters

HttpHeadersFilters

来实现。

继续阅读