API网关服务:Spring Cloud Zuul

API网关服务:Spring Cloud Zuul

简介

有前几章的介绍,我们对于Spring Cloud Netflix下的核心组件已经了解了一大半。这些组件基本涵盖了微服务架构中最为基础的几个核心设施,利用这些组件我们已经可以构建起一个简单的微服务架构系统,比如,通过使用Spring Cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现;通过Spring Cloud Ribbon或Feign实现服务间负载均衡的接口调用;同时,为了使分布式系统更为健壮,对于依赖的服务调用使用Spring Cloud Hystrix来进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。通过上述,可设计如下基础系统架构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GzlcuAXJ-1600188473133)(media/-----.png)]

在该架构中,我们的服务集群包含内部服务ServiceA和ServiceB, 它们都会向Eureka Server集群进行注册与订阅服务,而OpenService是一个对外的RESTfulAPI服务,它通过FS、 Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。

API网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、 负载均衡、 校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。

在SpringCloud中了提供了基于NetflixZuul实现的API网关组件Spring Cloud Zuul。那么,它是如何解决上面这两个普遍问题的呢?

首先,对于路由规则与服务实例的维护间题。SpringCloud Zuul通过与SpringCloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。
其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。SpringCloud Zuul提供了一套过滤器机制,它可以 很好地支持这样的任务。开发者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务应用可以更专注千业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。

简单来说,就是既具备路由转发功能,又具备过滤器功能,比如将/aaa/*路径请求转发到service-ribbon服务上,将/bbb/*路径请求转发到service-feign服务上,比如过滤,对请求参数的信息进行过滤,不符合的进行过滤拦截等。

快速入门

构建网关

添加依赖

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

添加@EnableZuulProxy注解

package com.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringCloudApplication
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }

}

配置属性文件

spring.application.name=api-gateway
server.port=5555

请求路由

传统路由配置方式

传统路由配置方式就是在不依赖与服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API网关对外部请求的路由。

单实例配置:通过zuul.routes..path与zuul.routes..url参数对的方式进行配置进行配置,比如:

#传统路由方式
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8001

该配置实现了对符合/api-a-url/**规则的请求路径转发到http://localhost:8001/地址的路由规则。比如

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SOjHBatu-1600188473134)(media/1546566370225.png)]

面向服务的路由

很显然,传统路由的配置方式对于我们来说并不友好,它同样需要运维人员花费时间来维护各个路由path与url的关系。为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝整合,我们可以让路由的path不是映射具体的url,而是让它映射到某个具体的服务,具体的url则交给Eureka的服务发现机制去自动维护,我们称这类路由为面向服务的路由。在Zuul中使用服务路由也同样简单,只需做下面这些配置。

引入依赖

为了与Eureka整合,我们需要在pom.xml中引入spring-cloud-starter-eureka依赖,具体如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
配置属性文件

application.properties配置文件中指定Eureka注册中心的位置,并且配置服务路由。具体如下:

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=HELLO-SERVICE

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=FEIGN-CONSUMER

eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
启动服务验证

在完成了上面单服务路由配置之后,我们可以将eureka-server、hello-service和feign-consumer以及这里用Spring Cloud Zuul构建的api-gateway都启动起来。启动完毕,在eureka-server的信息面板中,我们也可以看到,多了一个网关服务API-GATEWAY

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Vzx7K1d-1600188473135)(media/1546567452336.png)]

测试

访问http://localhost:5555/api-a/hello

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qoVVcWos-1600188473137)(media/1546567578994.png)]

http://localhost:5555/api-b/feign-consumer(这个需要多刷新几次)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mzv2jMay-1600188473138)(media/1546567625355.png)]

通过面向服务单路由配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的path与serviceId的映射组合,使得维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关可以自动化完成服务实例清单的维护,完美地解决了路由映射实例单维护问题。

请求过滤

简介

在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。**但是,每个客户端用户请求微服务的应用提供的接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开发。**然而,目前的服务路由并没有限制权限这样的功能,所有请求都会毫无保留地转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后系统的维护难度,因为同一个系统的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,**比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。**在完成剥离之后,有不少开发者会直接再微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了权限逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依然会存在。

对于这样的问题,**更好的做法是通过前置的网关服务来完成这些非业务性质的校验。**由于网关服务的假如,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度得到了相应降低。

为了再API网关中实现对客户端请求的校验,我们将继续介绍Spring Cloud Zuul的另一个核心功能:请求过滤。Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们之需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。

下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。

继承ZuulFilter

/**
 * 拦截器
 */
public class AccessFilter extends ZuulFilter {
    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        logger.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
        Object accessToken = request.getParameter("accessToken");
        if (accessToken == null) {
            logger.warn("access token is empty");
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(401);
            return null;
        }
        logger.info("access token ok");
        return null;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

}

在上面实现都过滤器代码中,我们通过继承ZuulFilter抽象类并重写下面4个方法来实现自定义都过滤器。这4个方法分别定义了如下内容。

  • filterType:过滤器都类型,它决定过滤器在请求都哪一个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行。
  • filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回都值来依次执行。
  • shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了true,因此过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
  • run:过滤器的具体逻辑。这里我们通过context.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后也可以进一步优化我们的返回,比如,通过context.setResponseBody(body)对返回的body内容进行编辑等。

添加启动类内容

在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器,比如,在应用启动类中增加如下内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RUp8owft-1600188473139)(media/1546568412683.png)]

测试

访问http://localhost:5555/api-a/hello

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aV8jigZp-1600188473139)(media/1546572456304.png)]

访问http://localhost:5555/api-a/hello?accessToken=token,偶尔会出现如下响应:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVJU0HZu-1600188473140)(media/1546572597348.png)]

到这里对于API网关服务的快速入门示例就完成了。通过对Spring Cloud Zuul两个核心功能的介绍,相信读者已经能够体会到API网关服务对微服务架构到重要性了,就目前掌握到API网关知识,我们可以将具体原因总结如下:

  • 它作为系统到统一入口,屏蔽了系统内部各个微服务的细节。
  • 它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡路由转发。
  • 它可以实现接口权限校验与微服务业务逻辑到解耦。
  • 通过服务网关中到过滤器,在各生命周期中去校验请求到内容,将原本在对外服务层做到校验前移,保证了微服务的无状态性,同时降低了微服务到测试难度,让服务本身集中关注业务逻辑的处理。

实际上,基于Spring Cloud Zuul实现到API网关服务除了上面所示的优点之外,它还有一些更加强大到功能,我们将在后面对其进行更深入的介绍。通过本节的内容,我们只是希望以一个简单到例子带领大家先来简单认识一下API网关服务提供的基础功能以及它在微服务架构中的重要地位。

路由详解

传统路由配置

所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API网关对外部请求的路由。

没有Eureka服务治理框架帮助的时候,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则:

单实例配置

单实例配置:通过一组zuul.routes.<route>.pathzuul.routes.<route>.url参数对的方式配置,比如:

zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8001

该配置实现了对符合/api-a-url/**规则的请求路径转发到http://localhost:8001/地址的路由规则,比如,当有一个请求http://localhost:5555/api-a-url/hello被发送到API网关上,由于/api-a-ur/hello能够被上述配置的path规则匹配,所以API网关会转发请求到http://localhost:8001/hello地址。

多实例配置

多实例配置:通过一组zuul.routes.<route>.pathzuul.routes.<route>.serviceId参数对的方式配置,比如:

zuul.routes.api-d.path=/ddd/**
zuul.routes.api-d.serviceId=hello
ribbon.eureka.enabled=false
hello.ribbon.listOfServers=http://localhost:8001/,http://localhost:8002/

该配置实现了对符合/ddd/**规则的请求路径转发到http://localhost:8001/http://localhost:8002/两个实例地址的路由规则。它的配置方式与服务路由的配置方式一样,都采用了zuul.routes.<route>.pathzuul.routes.<route>.serviceId参数对的映射方式,只是这里的serviceId是由用户手工命名的服务名称,配合<serviceId>.ribbon.listOfServers参数实现服务与实例的维护。由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略,于是这里还需要Spring Cloud Ribbon的配合。由于在Spring Cloud Zuul中自带了对Ribbon的依赖,所以我们只需要做一些配置即可,比如上面示例中关于Ribbon的各个配置,它们的具体作用如下:

  • ribbon.eureka.enabled:由于zuul.routes.<route>.serviceId指定的是服务名称,默认情况下Ribbon会根据服务发现机制来获取配置服务名对应的实例清单。但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false,不然配置的serviceId是获取不到对应实例清单的。
  • hello.ribbon.listOfServers:该参数内容与zuul.routes.<route>.serviceId的配置相对应,开头的hello对应了serviceId的值,这两个参数的配置相当于在该应用内部手工维护了服务与实例的对应关系。

不论是单实例还是多实例的配置方式,我们都需要为每一对映射关系指定一个名称,也就是上面配置中的<route>,每一个<route>就对应了一条路由规则。每条路由规则都需要通过path属性来定义一个用来匹配客户端请求的路径表达式,并通过urlserviceId属性来指定请求表达式映射具体实例地址或服务名。

服务路由配置

配置方式一

Spring Cloud Zuul通过与Spring Cloud Eureka的整合,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要向传统路由配置方式那样为serviceId去指定具体的服务实例地址,只需要通过一组zuul.routes.<route>.pathzuul.routes.<route>.serviceId参数对的方式配置即可。

比如下面的示例,它实现了对符合/api-a/**规则的请求路径转发到名为HELLO-SERVICE的服务实例上去的路由规则。其中<route>可以指定为任意的路由名称。

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=HELLO-SERVICE

配置方式二

对于面向服务的路由配置,除了使用pathserviceId映射的配置方式之外,还有一种更简洁的配置方式zuul.routes.<serviceId>=<path>,其中<serviceId>用来指定路由的具体服务名,<path>用来配置匹配的请求表达式。比如下面的例子,它的路由规则等价于上面通过pathserviceId组合使用的配置方式。

zuul.routes.HELLO-SERVICE=/api-a/**

**在Spring Cloud Netflix中,Zuul巧妙的整合了Eureka来实现面向服务的路由。实际上,我们可以直接将API网关也看做是Eureka服务治理下的一个普通微服务应用。它除了会将自己注册到Eureka服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。**所以,在Eureka的帮助下,API网关服务本身就已经维护了系统中所有serviceId与实例地址的映射关系。当有外部请求到达API网关的时候,根据请求的URL路径找到最佳匹配的path规则,API网关就可以知道要将该请求路由到哪个具体的serviceId上去。由于在API网关中已经知道serviceId对应服务实例的地址清单,那么只需要通过Ribbon的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。

服务路由的默认规则

虽然通过Eurekazuul的整合已经为我们省去了维护服务实例清单的大量配置工作,剩下来只需要再维护请求路径的匹配表达式与服务名映射关系即可。

但是实际的运用过程中发现,大部分的路由规则机会都会采用服务名作为外部请求的前缀,比如下面的列子,其中path路径的前缀使用了HELLO-SERVICE,而对应的服务名也是HELLO-SERVICE。

zuul.routes.HELLO-SERVICE.path=/HELLO-SERVICE/**
zuul.routes.HELLO-SERVICE.serviceId=HELLO-SERVICE

其实zuul已经自动的帮我们实现以服务名作为前缀的映射,我们不需要去配置它。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L15SgOUF-1600188473140)(media/1546662769944.png)]

但是,有一些服务我们不需要对外开发也被外部访问到了。这个时候我们可以使用zuul.ignore-services参数来设置一个服务名匹配表达式来定义不自动创建路由的规则zuul在自动创建服务路由的时候会根据该表达式来进行判断,如果服务名匹配表达式,那么zuul将跳过该服务,不为其创建路由规则。比如,设置为zuul.ignored-services=*的时候,zuul将对所有的服务都不自动创建路由规则。在这种情况下,我们就要在配置文件中为需要路由的服务添加路由规则(可以使用pathserviceId组合的配置方式,也可以使用更简洁的zuul.routes.<serviceId>=<path>配置方式),只有在配置文件中出现的映射规则会被创建路由,而从Eureka中获取的其他服务,zuul将不会为他们创建路由规则。

自定义路由映射规则

我们在构建微服服务系统的进行业务逻辑开发的时候,为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一般都会采用开闭原则来进行设计与开发。

**这使得系统在迭代过程中,有时候需要我们为一组互相配合的微服务定义一个版本标记来方便管理它们的版本关系,根据这个标记我们可以很容易的知道这些服务需要一起启动并配合使用。**比如:userservice-v1,userservice-v2,orderservice-v1,orderservice-v2等等。默认情况下,zuul自动为服务创建的路由表达式会采用服务名作为前缀,比如针对上面的userservice-v1userservice-v2,它会产生/userservice-v1/userservice-v2两个路径表达式来映射,这样生成出来的表示式规则单一,不利于管理。通常的做法就是为这些不同的版本的微服务应用生成以版本号作为路由前缀定义规则的路由规则,比如/v1/userservice/。这时候,通过这样具有版本号前缀的url路径,我们就可以很同意的通过路径表达式来归类和管理这些具有版本信息的微服务了。

我们可以使用zuul中自定义服务与路由映射关系的功能,创建类似于/v1/userserivce/**的路由匹配原则。

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
	return new PatternServiceRouteMapper(
			"(?<name>^.+)-(?<version>v.+$)",
			"${version}/${name}");
}

PatternServiceRouteMapper对象可以让开发者通过正则表达式来自定义服务与路由映射的生成关系。构造函数第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式,第二个参数是定义根据服务名中定义的内容转换出的路径表达式规则。当开发者在api网关中定义了PatternServiceRouteMapper实现之后,只需符合第一个参数定义规则的服务名,都会优先使用该实现构建出的表达式,如果没有匹配上的服务规则则还是会使用默认的路由映射规则,记采用完整服务名作为前缀的路径表达式。

路径匹配

ant风格路径表达式

在zuul中,路由匹配的路径表达式采用ant风格定义。

通配符说明
?匹配任意单个字符
*匹配任意数量的字符
**匹配任意数量的字符,支持多级目录

我们可以通过下表中的示例来进一步了解这三个通配符的含义并进行参考使用:

url路径说明
/user-service/?可以匹配/user-service/之后的一个人和字符的路径,比如/user-service/a,/user-service/b,/user-service/c
/user-service/*可以匹配/user-service/之后拼接的任意字符的路径,比如说/user-service/a,/user-service/aaa,无法匹配/user-service/a/b
/user-service/**可以匹配/user-service/*包含的内容之外,还可以匹配/user-service/a/b的多级目录

路径(服务)拆分

但是随着版本的迭代,对user-service服务做了一些功能拆分,将原本属于user-service服务的某些功能拆分到user-service-ext中去,而这些拆分的外部调用url路径希望能够复合/user-service/ext/**。这个时候

zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service

zuul.routes.user-service-ext.path=/user-service/ext/**
zuul.routes.user-service-ext.serviceId=user-service-ext

此时,调用user-service-ext服务的url路径实际上会同时被/user-service/**/user-service/ext/**两个表示式所匹配。在逻辑上,api网关优先选择/user-service/ext/**路由,然后再去匹配/user-service/**路由才能实现上述需求,但是如果使用上面的配置方式,实际上是无法保证这样的路由优先顺序的。

从下面的路由匹配算法中,我们可以看到它在使用路由规则匹配的请求路径的时候是通过线性便利的方法,在请求路径获取到第一个匹配的路由规则之后就返回并结束匹配过程。所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQacAEGJ-1600188473141)(media/5225109-464e39c0bd265df8.png)]

org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQM9hDX4-1600188473141)(media/5225109-933bbec621f5c21f.png)]

由于properties的配置内容无法保证有序,所以当出现这样的情况的时候,为了保证路由的优先顺序,我们需要使用yml文件来配置,以实现有序的路由规则

zuul:
  routes: 
    user-service-ext: 
      path: /user-service/ext/**
      serviceId: user-service-ext
    user-service:
      path: /user-service/**
      serviceId: user-service

忽略表达式

为了更细粒度和更为灵活地配置理由规则,zuul还提供了一个忽略表达式参数zuul.ignored-patterns。该参数可以用来设置不希望被api网关进行路由的url表达式。

如果我们不希望hello-service的/hello接口被路由,那么我们可以这样设置:

#忽略表达式
zuul.ignored-patterns=/**/hello/**
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=HELLO-SERVICE

访问http://localhost:5555/hello-service/hello?accessToken=token时,控制台上输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-29QvAvrP-1600188473142)(media/1546664384910.png)]

另外,该参数在使用时还需要注意它的范围并不是针对某个路由,而是对所有路由。所以在设置的时候需要全面考虑url规则,防止忽略了不该被忽略的url路径。

路由前缀

为了方便地为路由规则增加前缀信息,zuul提供了zuul.prefix参数来进行设置。比如,希望为网关上的路由规则增加/api前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api。另外,对于代理前缀会默认从路径中移除,我们可以通过设置zuul.strip-prefix=false来关闭该移除代理前缀的动作。也可以用zuul.routes..stripPrefix=false指定路由关闭移除代理前缀。

注意,在使用zuul.prefix参数的时候,目前的版本的实现还存在一些BUG,所以请谨慎使用,或是避开引发BUG的配置规则。具体引发BUG的规则如下:

假设我们设置zuul.prefix=/api,当路由的path表达式以/api开头的时候,将会产生错误的映射关系。可以通过如下实现来验证这个问题:

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=HELLO-SERVICE

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=FEIGN-CONSUMER

zuul.routes.api-c.path=/ccc/**
zuul.routes.api-c.serviceId=HELLO-SERVICE

这里配置了三个路由关系,当我们没有设置zuul.prefix=/api的时候,一切运行正常,但是增加了zuul.prefix=/api配置之后,会在控制台中得到下面这样的路由关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mkpzSV5F-1600188473142)(media/1546665635611.png)]

从日志信息中,我们可以看到,以/api开头的路由规则解析两个看似就有问题的映射url,我们可以通过该输出的url来访问,实际是路由得不到正确的服务接口的,只有非/api开头的路由规则/ccc/**能够正确路由。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kFxy0XWg-1600188473143)(media/1546665888824.png)]

上述实验基于brixton.sr7和camden.sr3测试均存在问题,所以在使用该版本或以下的时候,务必避免路由表达式的起始字符串与zuul.prefix参数相同。

本地跳转

在zuul实现的api网关路由功能中,还支持forward形式的服务端跳转配置。实现方式非常简单,只需要通过使用path与url的配置方式就能完成,通过url中使用forward来指定需要跳转的本地网关服务器资源路径。

在zuul-service服务中定义一个controller,

@RestController
public class HelloController {

   @RequestMapping("/local/hello")
   public String hello(){
       return "hello world local";
   }
}

配置文件配置:

#本地跳转
zuul.routes.api-d.path=/api-d/**
zuul.routes.api-d.serviceId=forward:/local

访问http://localhost:5555/api-d/hello?accessToken=token就跳转到了网关的/local/hello上了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i89ferhB-1600188473143)(media/1546666429161.png)]

Cookie与头信息

默认情况下,spring cloud zuul在请求路由时,会过滤掉http请求头信息中一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,默认包括cookie,set-Cookie,authorization三个属性。所以,我们在开发web项目时常用的cookie在spring cloud zuul网关中默认时不传递的,这就会引发一个常见的问题,如果我们要将使用了spring securityshiro等安全框架构建的web应用通过spring cloud zuul构建的网关来进行路由时,由于cookie信息无法传递,我们的web应用将无法实现登录和鉴权。为了解决这个问题,配置的方法有很多。

  • 通过设置全局参数为空来覆盖默认值,具体如下:
zuul.sensitiveHeaders=

这种方法不推荐,虽然可以实现cookie的传递,但是破坏了默认设置的用意。在微服务架构的api网关之内,对于无状态的restful api请求肯定时要远多于这些web类应用请求的,甚至还有一些架构设计会将web类应用和app客户端一样归为api网关之外的客户端应用。

  • 通过指定路由的参数来设置,方法有下面二种。
#方法一:对指定路由开启自定义敏感头
zuul.routes.<router>.customSensitiveHeaders=true
#方法二:对指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders=

比较推荐使用这二种方法,仅对指定的web应用开启对敏感信息的传递,影响范围小,不至于引起其他服务的信息泄露问题。

重定向问题

在使用Spring Cloud Zuul对接Web网站的时候,处理完了会话控制问题之后。往往我们还会碰到如下图所示的问题,我们在浏览器中通过Zuul发起了登录请求,该请求会被路由到某WebSite服务,该服务在完成了登录处理之后,会进行重定向到某个主页或欢迎页面。此时,仔细的开发者会发现,在登录完成之后,我们浏览器中URL的HOST部分发生的改变,该地址变成了具体WebSite服务的地址了。这就是在这一节,我们将分析和解决的重定向问题!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEpajLpT-1600188473143)(media/20181225170004446)]

出现该问题的根源是Spring Cloud Zuul没有正确的处理HTTP请求头信息中的Host导致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter过滤器实现时完全没有考虑这一问题,它更多的定位于REST API的网关。所以如果要在Brixton版本中增加这一特性就相对较为复杂,不过好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增强了该功能,我们只需要通过配置属性zuul.add-host-header=true就能让原本有问题的重定向操作得到正确的处理。

Hystrix和Ribbon支持

1. 设置Hystrix超时时间

使用hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置API网关中路由转发请求的命令执行时间超过配置值后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常,Zuul会对该异常进行处理并返回如下JSON信息给外部调用方

{
    "timestamp":20180705141032,
    "status":500,
    "error":"Internal Server Error",
    "exception":"com.netflix.zuul.exception.ZuulException",
    "message":"TIMEOUT"
}

2. 设置Ribbon连接超时时间

使用ribbon.ConnectTimeout参数创建请求连接的超时时间,当ribbon.ConnectTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds的配置值时,若出现请求超时的时候,会自动进行重试路由请求,如果依然失败,Zuul会返回如下JSON信息给外部调用方

{
    "timestamp":20180705141032,
    "status":500,
    "error":"Internal Server Error",
    "exception":"com.netflix.zuul.exception.ZuulException",
    "message":"NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED"
}

如果ribbon.ConnectTimeout的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds的配置值时,当出现请求超时的时候不会进行重试,直接超时处理返回TIMEOUT的错误信息

3. 设置Ribbon的请求转发超时时间

使用ribbon.ReadTimeout来设置请求转发超时时间,处理与ribbon.ConnectTimeout类似,不同点在于这是连接建立之后的处理时间。该值小于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds的配置值时报TIMEOUT错误,反之报TIMEOUT的错误。小于的时候会先重试,不成才报错;大于的时候直接报错。

4. 关闭重试配置

  • 全局配置zuul.retryable=false
  • 针对路由配置zuul.routes.<路由名>.retryable=false

过滤器详解

过滤器

在快速入门的请求过滤这一节的学习中,我们使用的pre类型的过滤器,那时我们就比较好奇:为什么不用route或其它类型?

实际上,路由运行过程中,它的路由映射和请求转发是由几个不同的过滤器完成的。其中,

  • pre类型的过滤器用于路由的映射,将请求路径和路由规则进行匹配,用以找到需要转发目标地址。
  • route类型的过滤器用来请求转发,从pre类型过滤器获取的目标地址进行转发。

在Spring Cloud Zuul 中实现过滤器必须包含4 个基本特征:过滤类型、执行顺序、执行条件、具体操作。实际上就是ZuulFilter抽象类中定义的抽象方法:

String filterType();
int filterOrder();
boolean shouldFilter();
Object run();

这里分别进行解释:

  • filterType:该方法需要返回一个字符串来代表过滤器的类型,而这个类型就是Zuul中的4种不同生命周期的过滤器类型,如下
    • pre:在请求到达路由前被调用
    • route:在路由请求时被调用
    • error: 处理请求时发生的错误时被调用。
    • post:在route和error过滤器之后被调用,最后调用。
  • filterOrder:通过int值定义过滤器执行顺序,数值越小优先级越高。
  • shouldFilter:返回布尔值来判断该过滤器是否执行。
  • run:过滤器的具体逻辑。可以在此确定是否拦截当前请求等。

请求的生命周期

和书的作者一样,我也去爬了下Zuul的官方wiki,这里我们简化梳理一下流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O46wZZDp-1600188473144)(media/1149398-20180709105524084-653519683.png)]

首先HTTP请求到达Zuul,最先来到pre过滤器,在这里会去映射url patern到目标地址上然后将请求与找到的地址交给route类型的过滤器进行求转发,请求服务实例获取响应,通过post类型过滤器对处理结果进行加工与转换等操作返回。error类型的过滤器比较特殊,在这整个请求过程中只要有异常才会触发,将异常结果交给post类型过滤器加工返回

routing在filterType方法返回的类型是"route",不是routing,查官方代码发现的

禁用过滤器

说到禁用过滤器,第一想到的是自定义的过滤器中shouldFilter返回false,实际应用中,这样还需要重新编译代码。

Zuul贴心地提供了一个参数用来禁用指定过滤器,zuul.<过滤器名>.<过滤器类型>.disable=true

之前我们的实验必须headers中有accessToken才能通过AccessFilter,现在我们禁用一下试试

#禁用过滤器
zuul.AccessFilter.pre.disable:true

测试结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sLcOSyZO-1600188473144)(media/1546668102893.png)]

核心过滤器

在Spring Cloud Zuul中,为了让API网关组件可以更方便的上手使用,它在HTTP请求生命周期的各个阶段默认地实现了一批核心过滤器,它们会在API网关服务启动的时候被自动地加载和启用。我们可以在源码中查看和了解它们,它们定义于spring-cloud-netflix-core模块的org.springframework.cloud.netflix.zuul.filters包下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0pUxUNjM-1600188473145)(media/zuul-default-filter.png)]

如上图所示,在默认启用的过滤器中包含了三种不同生命周期的过滤器,这些过滤器都非常重要,可以帮助我们理解Zuul对外部请求处理的过程,以及帮助我们如何在此基础上扩展过滤器去完成自身系统需要的功能。下面,我们将逐个地对这些过滤器做一些详细的介绍:

pre过滤器
  • ServletDetectionFilter:它的执行顺序为-3,是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行,还是通过ZuulServlet来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样在后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法判断它以实现做不同的处理。一般情况下,发送到API网关的外部请求都会被Spring的DispatcherServlet处理,除了通过/zuul/路径访问的请求会绕过DispatcherServlet,被ZuulServlet处理,主要用来应对处理大文件上传的情况。另外,对于ZuulServlet的访问路径/zuul/,我们可以通过zuul.servletPath参数来进行修改。
  • Servlet30WrapperFilter:它的执行顺序为-2,是第二个执行的过滤器。目前的实现会对所有请求生效,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象。
  • FormBodyWrapperFilter:它的执行顺序为-1,是第三个执行的过滤器。该过滤器仅对两种类请求生效,第一类是Content-Type为application/x-www-form-urlencoded的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象。
  • DebugFilter:它的执行顺序为1,是第四个执行的过滤器。该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。而它的具体操作内容则是将当前的请求上下文中的debugRouting和debugRequest参数设置为true。由于在同一个请求的不同生命周期中,都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过请求参数的方式来激活这些debug信息以帮助分析问题。另外,对于请求参数中的debug参数,我们也可以通过zuul.debug.parameter来进行自定义。
  • PreDecorationFilter:它的执行顺序为5,是pre阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的)。而它的具体操作内容就是为当前请求做一些预处理,比如:进行路由规则的匹配、在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息。另外,我们还可以在该实现中找到一些对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如:X-Forwarded-Host、X-Forwarded-Port。另外,对于这些头域的记录是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值为true,所以Zuul在请求跳转时默认地会为请求增加X-Forwarded-*头域,包括:X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-For、X-Forwarded-Prefix、X-Forwarded-Proto。我们也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作。

《Spring Cloud实战小贴士:Zuul处理Cookie和重定向》 一文中提到的加载敏感头信息加入到忽略头信息的操作调用就在PreDecorationFilter过滤器中实现。

route过滤器
  • RibbonRoutingFilter:它的执行顺序为10,是route阶段第一个执行的过滤器。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回。
  • SimpleHostRoutingFilter:它的执行顺序为100,是route阶段第二个执行的过滤器。该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。
  • SendForwardFilter:它的执行顺序为500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中的forward本地跳转配置。
post过滤器
  • SendErrorFilter:它的执行顺序为0,是post阶段第一个执行的过滤器。该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用请求上下文中的错误信息来组织成一个forward到API网关/error错误端点的请求来产生错误响应。
  • SendResponseFilter:它的执行顺序为1000,是post阶段最后执行的过滤器。该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只有在包含它们其中一个的时候就会执行处理逻辑。而该过滤器的处理逻辑就是利用请求上下文的响应信息来组织需要发送回客户端的响应内容。

**这里不列出具体代码了,读者可自行根据类名来查看源码了解详细处理过程。**下图是对上述过滤器根据顺序、名称、功能、类型做了综合的整理,可以帮助我们在自定义过滤器或是扩展过滤器的时候用来参考并全面地考虑整个请求生命周期的处理过程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N90JUOjk-1600188473145)(media/zuul-filter-core.png)]

异常处理

在上一节中,我们详细介绍了Spring Cloud Zuul中自己实现的一些核心过滤器,以及这些过滤器在请求生命周期中的不同作用。我们会发现在这些核心过滤器中并没有实现error阶段的过滤器。那么这些过滤器可以用来做什么呢?接下来,本文将介绍如何利用error过滤器来实现统一的异常处理。

过滤器中抛出异常的问题

首先,我们可以来看看默认情况下,过滤器中抛出异常Spring Cloud Zuul会发生什么现象。我们创建一个pre类型的过滤器,并在该过滤器的run方法实现中抛出一个异常。比如下面的实现,在run方法中调用的doSomething方法将抛出RuntimeException异常。

public class ThrowExceptionFilter extends ZuulFilter  {

    private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        log.info("This is a pre filter, it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething() {
        throw new RuntimeException("Exist some errors...");
    }
  
}

运行网关程序并访问某个路由请求,此时我们会发现:在API网关服务的控制台中输出了ThrowExceptionFilter的过滤逻辑中的日志信息,但是并没有输出任何异常信息,同时发起的请求也没有获得任何响应结果。为什么会出现这样的情况呢?我们又该如何在过滤器中处理异常呢?

解决方案一:严格的try-catch处理

回想一下,我们在上一节中介绍的所有核心过滤器,是否还记得有一个post过滤器SendErrorFilter是用来处理异常信息的?根据正常的处理流程,该过滤器会处理异常信息,那么这里没有出现任何异常信息说明很有可能就是这个过滤器没有被执行。所以,我们不妨来详细看看SendErrorFiltershouldFilter函数:

public boolean shouldFilter() {
	RequestContext ctx = RequestContext.getCurrentContext();
	return ctx.containsKey("error.status_code") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}

可以看到该方法的返回值中有一个重要的判断依据ctx.containsKey("error.status_code"),也就是说请求上下文中必须有error.status_code参数,我们实现的ThrowExceptionFilter中并没有设置这个参数,所以自然不会进入SendErrorFilter过滤器的处理逻辑。那么我们要如何用这个参数呢?我们可以看一下route类型的几个过滤器,由于这些过滤器会对外发起请求,所以肯定会有异常需要处理,比如RibbonRoutingFilterrun方法实现如下:

public Object run() {
	RequestContext context = RequestContext.getCurrentContext();
	this.helper.addIgnoredHeaders();
	try {
		RibbonCommandContext commandContext = buildCommandContext(context);
		ClientHttpResponse response = forward(commandContext);
		setResponse(response);
		return response;
	}
	catch (ZuulException ex) {
		context.set(ERROR_STATUS_CODE, ex.nStatusCode);
		context.set("error.message", ex.errorCause);
		context.set("error.exception", ex);
	}
	catch (Exception ex) {
		context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		context.set("error.exception", ex);
	}
	return null;
}

可以看到,整个发起请求的逻辑都采用了try-catch块处理。在catch异常的处理逻辑中并没有做任何输出操作,而是往请求上下文中添加一些error相关的参数,主要有下面三个参数:

  • error.status_code:错误编码
  • error.exceptionException异常对象
  • error.message:错误信息

其中,error.status_code参数就是SendErrorFilter过滤器用来判断是否需要执行的重要参数。分析到这里,实现异常处理的大致思路就开始明朗了,我们可以参考RibbonRoutingFilter的实现对ThrowExceptionFilterrun方法做一些异常处理的改造,具体如下:

public Object run() {
    log.info("This is a pre filter, it will throw a RuntimeException");
    RequestContext ctx = RequestContext.getCurrentContext();
    try {
        doSomething();
    } catch (Exception e) {
        ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        ctx.set("error.exception", e);
    }
  	return null;
}

通过上面的改造之后,我们再尝试访问之前的接口,这个时候我们可以得到如下响应内容:

{
  "timestamp": 1481674980376,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "java.lang.RuntimeException",
  "message": "Exist some errors..."
}

此时,我们的异常信息已经被SendErrorFilter过滤器正常处理并返回给客户端了,同时在网关的控制台中也输出了异常信息。从返回的响应信息中,我们可以看到几个我们之前设置在请求上下文中的内容,它们的对应关系如下:

  • status:对应error.status_code参数的值
  • exception:对应error.exception参数中Exception的类型
  • message:对应error.exception参数中Exceptionmessage信息。对于message的信息,我们在过滤器中还可以通过ctx.set("error.message", "自定义异常消息");来定义更友好的错误信息。SendErrorFilter会优先取error.message来作为返回的message内容,如果没有的话才会使用Exception中的message信息

解决方案二:ErrorFilter处理

通过上面的分析与实验,我们已经知道如何在过滤器中正确的处理异常,让错误信息能够顺利地流转到后续的SendErrorFilter过滤器来组织和输出。但是,即使我们不断强调要在过滤器中使用try-catch来处理业务逻辑并往请求上下文添加异常信息,但是不可控的人为因素、意料之外的程序因素等,依然会使得一些异常从过滤器中抛出,对于意外抛出的异常又会导致没有控制台输出也没有任何响应信息的情况出现,那么是否有什么好的方法来为这些异常做一个统一的处理呢?

这个时候,我们就可以用到error类型的过滤器了。由于在请求生命周期的preroutepost三个阶段中有异常抛出的时候都会进入error阶段的处理,所以我们可以通过创建一个error类型的过滤器来捕获这些异常信息,并根据这些异常信息在请求上下文中注入需要返回给客户端的错误描述,这里我们可以直接沿用在try-catch处理异常信息时用的那些error参数,这样就可以让这些信息被SendErrorFilter捕获并组织成消息响应返回给客户端。比如,下面的代码就实现了这里所描述的一个过滤器:

public class ErrorFilter extends ZuulFilter {

    Logger log = LoggerFactory.getLogger(ErrorFilter.class);

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable throwable = ctx.getThrowable();
        log.error("this is a ErrorFilter : {}", throwable.getCause().getMessage());
        ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        ctx.set("error.exception", throwable.getCause());
        return null;
    }

}

在将该过滤器加入到我们的API网关服务之后,我们可以尝试使用之前介绍try-catch处理时实现的ThrowExceptionFilter(不包含异常处理机制的代码),让该过滤器能够抛出异常。这个时候我们再通过API网关来访问服务接口。此时,我们就可以在控制台中看到ThrowExceptionFilter过滤器抛出的异常信息,并且请求响应中也能获得如下的错误信息内容,而不是什么信息都没有的情况了。

{
  "timestamp": 1481674993561,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "java.lang.RuntimeException",
  "message": "Exist some errors..."
}

动态加载

动态路由需要达到可持久化配置,动态刷新的效果。不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果

动态路由

如何实现Zuul的动态路由,我们很自然的会将它与spring cloud config的动态刷新机制联系到一起。只需将API网关服务的配置文件通过spring cloud config连接到git仓库存储和管理,我们就能轻松实现动态刷新路由规则的功能。

创建config-service模块

导入依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
</dependencies>

配置属性文件
spring.application.name=config-server
server.port=7001

# git管理配置
spring.cloud.config.server.git.uri=https://github.com/ZhongZeWei/springCloudConfig.git
spring.cloud.config.server.git.searchPaths=config-repo/
spring.cloud.config.server.git.username=自己的GitHub账户
spring.cloud.config.server.git.password=自己的GitHub密码

设置启动类
package com.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }

}



创建api-gateway-dynamic-route模块

导入依赖

需要导入zuul、eureka、config的依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

    </dependencies>

创建并配置bootstrap.properties配置文件
spring.application.name=api-gateway
server.port=5556

spring.cloud.config.uri=http://localhost:7001/

eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

设置启动类
package com.apigatewaydynamicroute;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayDynamicRouteApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayDynamicRouteApplication.class, args);
    }

    @Bean
    @RefreshScope
    @ConfigurationProperties("zuul")
    public ZuulProperties zuulProperties() {
        return new ZuulProperties();
    }
}



创建git下的配置文件上传GitHub

在api-gateway.properties写入以下内容,并上传至GitHub的指定目录:

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=HELLO-SERVICE

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=FEIGN-CONSUMER

zuul.routes.api-c.path=/ccc/**
zuul.routes.api-c.serviceId=HELLO-SERVICE

测试

启动红框中的服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRK333Jb-1600188473146)(media/1546676903204.png)]

访问http://localhost:5556/routes

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L2zmx1xk-1600188473146)(media/1546676161210.png)]

访问http://localhost:5556/api-a/hello?accessToken=token,偶尔会得到以下结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-57MD7dQK-1600188473147)(media/1546676053020.png)]

刷新配置

修改api-gateway.properties内容,并上传至GitHub的指定目录:

#api-a变为api-aa
zuul.routes.api-a.path=/api-aa/**
zuul.routes.api-a.serviceId=HELLO-SERVICE

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=FEIGN-CONSUMER

zuul.routes.api-c.path=/ccc/**
zuul.routes.api-c.serviceId=HELLO-SERVICE

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2YB3aKhp-1600188473147)(media/1546676327272.png)]

使用post请求访问api-gateway-dynamic-route的/refresh端口,当配置文件有修改时,该接口会返回被修改的属性名称:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6z2ee8k8-1600188473148)(media/1546676551500.png)]

访问http://localhost:5556/routes

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWTmnR7d-1600188473148)(media/1546676600318.png)]

访问http://localhost:5556/api-aa/hello?accessToken=token

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yxPG8Vsh-1600188473149)(media/1546676659689.png)]

动态过滤器

zuul 支持过滤器动态修改动态加载功能,目前支持动态Filter由Groovy编写。动态管理Groovy编写的Filter类文件变更以及动态编译等功能。zuul通过定期扫描Filter文件存放的目录来校验是否有新的文件或有改动的文件,对这些文件进行加载,从而实现动态加载动态过滤器。

创建api-gateway-dynamic-filter工程

导入依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-all</artifactId>
       <version>2.4.7</version>
    </dependency>
</dependencies>

配置属性文件
spring.application.name=api-gateway
server.port=5555

zuul.routes.hello.path=/hello-service/**
zuul.routes.hello.serviceId=HELLO-SERVICE

eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

zuul.filter.root=C:/Users/22051/Desktop/chapter7/api-gateway-dynamic-filter/filter
zuul.filter.interval=5

zuul.filter.root是需要加载的过滤器文件夹位置,zuul.filter.interval是动态加载间隔时间。

配置启动类

在启动类中,引用FilterConfiguration配置,并创建动态加载过滤器的实例:

package com.apigatewaydynamicfilter;

import com.netflix.zuul.FilterFileManager;
import com.netflix.zuul.FilterLoader;
import com.netflix.zuul.groovy.GroovyCompiler;
import com.netflix.zuul.groovy.GroovyFileFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;

@EnableZuulProxy
@EnableConfigurationProperties({FilterConfiguration.class})
@SpringBootApplication
public class ApiGatewayDynamicFilterApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayDynamicFilterApplication.class, args);
    }

    @Bean
    public FilterLoader filterLoader(FilterConfiguration filterConfiguration) {
        FilterLoader filterLoader = FilterLoader.getInstance();
        filterLoader.setCompiler(new GroovyCompiler());
        try {
            FilterFileManager.setFilenameFilter(new GroovyFileFilter());
            FilterFileManager.init(
                    filterConfiguration.getInterval(),
                    filterConfiguration.getRoot() + "/pre",
                    filterConfiguration.getRoot() + "/post");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return filterLoader;
    }
}



创建动态加载的过滤器

创建filter文件夹,将PostFilter.groovy、PreFilter.groovy放进去。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BrqdX7jc-1600188473149)(media/1546678657585.png)]

测试动态加载过滤器

访问http://localhost:5555/hello-service/hello?accessToken=token

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H1pNGGqU-1600188473149)(media/1546678777834.png)]

修改PostFilter.groovy内容,如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iDWjTPbS-1600188473150)(media/1546678823685.png)]

不重启服务的情况下,再次访问http://localhost:5555/hello-service/hello?accessToken=token,可以发现结果得到了改变:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WnNsbRE4-1600188473150)(media/1546678889779.png)]

注意事项

通过本节对动态过滤器加载的内容的介绍,可以看出,API网关服务的动态过滤器功能可以帮助我们增强API网关的持续服务能力,对于网关中的处理逻辑维护也变得更为灵活。但目前的版本下,动态过滤器还是一个半成品,目前的动态过滤器无法直接注入API网关服务的spring容器中加载的实例来使用的,比如,我们无法直接注入RestTemplate等实例,在动态过滤器中对各个服务发起请求的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值