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

1. SpringCloud Gateway

先来了解一些概念。

1. 简介

SpringCloud Gateway是一个建立在Spring生态之上,基于Spring5Spring Boot 2Project Reactor的API网关。目标是提供一个简单但是有效的方式把请求路由到API,并提供像是安全、监控/指标和弹性之类的值得关注的切面。

2. 几个术语

  • Route:网关的基本构件。由一个ID,一个目标URI,一个predicates的集合,一个filters的集合组成。当predicates总体判断是true的时候,这个route就算匹配成功了。

  • Predicate:这是一个Java8 Function Predicate。输入类型是一个Spring FrameWork ServerWebExchange。这允许您匹配来自HTTP请求的任何内容,比如通过请求头或者请求参数。

  • Filter:是由特定的工厂类构建的GatewayFilter的实例。在发送下游请求之前或者之后,你可以在其中对请求或者响应做修改。

    说明:如果在routeURI里面没有定义端口的话,HTTP和HTTPS的端口默认分别会被解析为为80和443。

3. SpringCloud Gateway是如何工作的

下面的这张图片整体描述了SpringCloud Gateway是如何工作的:

【微服务学习】用SpringCloud Gateway做一个动态API网关_第1张图片

客户端向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进行解析处理。


<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.0modelVersion>

    <groupId>org.makabakagroupId>
    <artifactId>apigatewayartifactId>
    <version>1.0-SNAPSHOTversion>

    <parent>
        <artifactId>spring-boot-starter-parentartifactId>
        <groupId>org.springframework.bootgroupId>
        <version>2.2.10.RELEASEversion>
    parent>
f
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>Hoxton.SR4version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>5.8.5version>
        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("/**", "/?(?.*)");
                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 GatewayRoutePredicateFilter实现了动态路由配置,刷新功能。但一个实际使用的API网关的需求可能还包括鉴权、请求记录、流量统计等等。这些都可以使用SpringCloud Gateway的其它特性,比如GlobalFiltersHttpHeadersFilters来实现。

你可能感兴趣的:(微服务学习,spring,cloud,gateway)