SpringCloud07—API网关服务:Spring Cloud Zuul

╰+哭是因爲堅強的太久メ 2023-10-11 16:16 111阅读 0赞

上一篇:《SpringCloud06—声明式服务调用:Spring Cloud Feign》

7.API网关服务:Spring Cloud Zuul

7.1 背景介绍

通过前几章的介绍,我们对于Spring Cloud Netflix下的核心组件已经了解了一大半。这些组件基本涵盖了微服务架构中最为基础的几个核心设施,利用这些组件我们已经可以构建起一个简单的微服务架构系统
比如:

  • 通过使用Spring Cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现;
  • 通过Spring Cloud Ribbon或Feign实现服务间负载均衡的接口调用;
  • 同时,为了使分布式系统更为健壮,对于依赖的服务调用使用SpringCloud Hystrix来进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。

通过上述思路,我们可以设计出类似下图的基础系统架构。
在这里插入图片描述

在本章中,我们将把视线聚焦在对外服务这块内容,通常也称为边缘服务。
首先,我们从运维人员的角度来看看,他们平时都需要做一些什么工作来支持这样的架构。
当客户端应用单击某个功能的时候往往会发出一些对微服务获取资源的请求到后端,这些请求通过F5、Nginx等设施的路由和负载均衡分配后,被转发到各个不同的服务实例上。
而为了让这些设施能够正确路由与分发请求,运维人员需要手工维护这些路由规则与服务实例列表,当有实例增减或是IP地址变动等情况发生的时候,也需要手工地去同步修改这些信息以保持实例信息与中间件配置内容的一致性。
在系统规模不大的时候,维护这些信息的工作还不会太过复杂,但是如果当系统规模不断增大,那么这些看似简单的维护任务会变得越来越难,并且出现配置错误的概率也会逐渐增加。很显然,这样的做法并不可取,所以我们需要一套机制来有效降低维护路由规则与服务实例列表的难度。
其次,我们再从开发人员的角度来看看,在这样的架构下,会产生一些怎样的问题呢?
大多数情况下,为了保证对外服务的安全性,我们在服务端实现的微服务接口,往往都会有一定的权限校验机制,比如对用户登录状态的校验等;
同时为了防止客户端在发起请求时被篡改等安全方面的考虑,还会有一些签名校验的机制存在。
这时候,由于使用了微服务架构的理念,我们将原本处于一个应用中的多个模块拆成了多个应用,但是这些应用提供的接口都需要这些校验逻辑,我们不得不在这些应用中都实现这样一套校验逻辑。
随着微服务规模的扩大,这些校验逻辑的冗余变得越来越多,突然有一天我们发现这套校验逻辑有个BUG需要修复,或者需要对其做一些扩展和优化,此时我们就不得不去每个应用里修改这些逻辑,而这样的修改不仅会引起开发人员的抱怨,更会加重测试人员的负担。所以,我们也需要一套机制能够很好地解决微服务架构中,对于微服务接口访问时各前置校验的冗余问题。
为了解决上面这些常见的架构问题,API 网关的概念应运而生。
API网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。
它能够实现:

  • 请求路由
  • 负载均衡
  • 校验过滤等功能

除此之外还能够

  • 与服务治理框架的结合
  • 请求转发时的熔断机制
  • 服务的聚合等一系列高级功能。

7.2 快速入门

7.2.1 构建网关

首先,在实现各种 API网关服务的高级功能之前,我们需要做一些准备工作,比如,构建起最基本的API网关服务,并且搭建几个用于路由和过滤使用的微服务应用等。
对于微服务应用,我们可以直接使用之前章节实现的hello-servicefeign-consumer
虽然之前我们一直将feign-consumer视为消费者,但是在 Eureka的服务注册与发现体系中,每个服务既是提供者也是消费者,所以feign-consumer实质上也是一个服务提供者。
之前我们访问的http://localhost:9001/feign-consumer等一系列接口就是它提供的服务。
这里,我们详细介绍一下API 网关服务的构建过程。

1.创建一个基础的Spring Boot工程,命名为api-gateway

在pom.xml中引入spring-cloud-starter-zuul依赖,具体如下:

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>2.4.8</version>
  5. <relativePath/> <!-- lookup parent from repository -->
  6. </parent>
  7. <groupId>com.cloud</groupId>
  8. <artifactId>api-gateway</artifactId>
  9. <version>0.0.1-SNAPSHOT</version>
  10. <name>api-gateway</name>
  11. <properties>
  12. <java.version>11</java.version>
  13. </properties>
  14. <dependencies>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-web</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-test</artifactId>
  22. <scope>test</scope>
  23. </dependency>
  24. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-client -->
  25. <dependency>
  26. <groupId>org.springframework.cloud</groupId>
  27. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  28. </dependency>
  29. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-zuul -->
  30. <dependency>
  31. <groupId>org.springframework.cloud</groupId>
  32. <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  33. <version>2.2.9.RELEASE</version>
  34. </dependency>
  35. </dependencies>

spring-cloud-starter-netflix-zuul依赖,可以通过查看它的依赖内容了解到:
该模块中不仅包含了Netflix Zuul的核心依赖zuul-core,它还包含了下面这些网关服务需要的重要依赖。

  • spring-cloud-starter-hystrix: 该依赖用来在网关服务中实现对微服务转发时候的保护机制,通过线程隔离和断路器,防止微服务的故障引发API网关资源无法释放,从而影响其他应用的对外服务。
  • spring-cloud-starter-ribbon: 该依赖用来实现在网关服务进行路由转发时候的客户端负载均衡以及请求重试。
  • spring-boot-starter-actuator: 该依赖用来提供常规的微服务管理端点。另外,在Spring Cloud Zuul中还特别提供了/routes端点来返回当前的所有路由规则。
2.在主应用类上添加注解
  1. import org.springframework.boot.SpringApplication;
  2. import org.springframework.boot.autoconfigure.SpringBootApplication;
  3. import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
  4. @SpringBootApplication
  5. @EnableZuulProxy
  6. @EnableEurekaClient
  7. public class ApiGatewayApplication {
  8. public static void main(String[] args) {
  9. SpringApplication.run(ApiGatewayApplication.class, args);
  10. }
  11. }

使用@EnablezuulProxy注解开启Zuul的API 网关服务功能

3.在application.properties中配置Zuul应用的基础信息

如应用名、服务端口号等,具体内容如下:

  1. server.port=5555
  2. spring.application.name=api-getway
  3. # 注册服务的时候使用服务的ip地址
  4. eureka.instance.prefer-ip-address=false
  5. eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
  6. # 前缀,用来做版本控制
  7. zuul.prefix=/v1
  8. # 禁用默认路由,执行配置路由
  9. zuul.ignored-services="*"

7.2.2 请求路由

在application.properties添加以下配置就可以实现传统的路由转发功能

  1. # 配置hello-service的服务路由
  2. zuul.routes.api-a.path=/api-a/**
  3. zuul.routes.api-a.service-id=hello-service
  4. # feign-consumer的服务路由
  5. zuul.routes.api-b.path=/api-b/**
  6. zuul.routes.api-b.service-id=feign-consumer

下面我大致解释一下以上代码的意思
在这里插入图片描述

  • http://localhost:5555/api-a/hello: 该url符合/api-a/**规则,由api-a路由负责转发,该路由映射的serviceld为hello-service,所以最终/hello 请求会被发送到hello-service服务的某个实例上去,也就是对应的/hello接口
    在这里插入图片描述
  • http:/ /localhost:5555/api-b/feign-consumer: 该url符合/api-b/**规则,由api-b路由负责转发,该路由映射的serviceId为feign-consumer,所以最终/feign-consumer请求会被发送到feign-consumer服务的某个实例上去。

7.3 请求过滤

在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。
但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开放。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会被毫无保留地转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后系统的维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成了剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依然会存在。
对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度也得到了相应降低。
为了在API网关中实现对客户端请求的校验,我们将继续介绍Spring Cloud Zuul的另外一个核心功能:请求过滤
Zuul 允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了
下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。

  1. import com.netflix.zuul.ZuulFilter;
  2. import com.netflix.zuul.context.RequestContext;
  3. import com.netflix.zuul.exception.ZuulException;
  4. import javax.servlet.http.HttpServletRequest;
  5. @Component
  6. public class AccessFilter extends ZuulFilter {
  7. private final String tokenSign = "accessToken";
  8. private static Logger log= LoggerFactory.getLogger(AccessFilter.class);
  9. @Override
  10. public String filterType() {
  11. return "pre";
  12. }
  13. @Override
  14. public int filterOrder() {
  15. return 0;
  16. }
  17. @Override
  18. public boolean shouldFilter() {
  19. return true;
  20. }
  21. @Override
  22. public Object run() throws ZuulException {
  23. RequestContext ctx = RequestContext.getCurrentContext();
  24. HttpServletRequest request = ctx.getRequest();
  25. log.info("send{} request to {}", request.getMethod(), request.getRequestURL().toString());
  26. Object accessToken = request.getHeader(tokenSign);
  27. if (accessToken == null) {
  28. log.warn("token is empty");
  29. ctx.setSendZuulResponse(false);
  30. ctx.setResponseStatusCode(401);
  31. return null;
  32. }
  33. log.info("token is Ok");
  34. return null;
  35. }
  36. }

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

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

重启项目,访问http://localhost:5555/v1/api-a/hello
在这里插入图片描述

由于需要在请求头设置token,所以我们该用postman进行测试
在这里插入图片描述

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

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

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

  • 通过设置全局参数为空来覆盖默认值保证通过网关访问服务时可以携带cookie,具体如下:

    设置全局参数为空来覆盖默认值

    zuul.sensitive-headers=

  • 对指定路由开启自定义敏感头

    对指定路由开启自定义敏感头

    zuul.routes..customSensitiveHeaders=true

  • 将指定路由的敏感头信息设置为空

    zuul.routes..sensitiveHeaders=[这里设置要过滤的敏感头]

7.5 对于HyStrix和Ribbon的设置

Zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。
但是需要注意,当使用path与ur1的映射关系来配置路由规则的时候,对于路由转发的请求不会采用HystrixCommand来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置,这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。

  • 1.启用hystrix配置

    hystrix.metrics.enabled=true

  • 2.设置API网关中路由转发请求的HystrixCommand执行超时时间,单位为毫秒

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

当路由转发请求的命令执行时间超过该配置值之后,Hystrix 会将该执行命令标记为TIMEOUT并抛出异常,Zuul会对该异常进行处理并返回如下JSON信息给外部调用方。

  1. {
  2. "timestamp": "2021-07-15T07:25:21.723+00:00",
  3. "status": 504,
  4. "error": "Gateway Timeout",
  5. "message": ""
  6. }
  • 3.路由转发请求时,创建请求连接的超时时间 ,单位ms

    ribbon.ConnectTimeout=3000

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

  1. {
  2. "timestamp":1481352582852,
  3. "status":500,
  4. "error":"Internal Server Error",
  5. "exception":"com. netflix. zuul. exception. ZuulException",
  6. "message":"NUMBEROF RETRIES NEXTSERVER EXCEEDED"
  7. }

如果ribbon .ConnectTimeout的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,当出现路由请求连接超时时,由于此时对于路由转发的请求命令已经超时,所以不会进行重试路由请求,而是直接按请求命令超时处理,返回TIMEOUT的错误信息。

  1. {
  2. "timestamp": "2021-07-15T07:42:51.028+00:00",
  3. "status": 500,
  4. "error": "Internal Server Error",
  5. "message": ""
  6. }
  • 4.设置路由的重试机制 默认是true

    zuul.retryable=false

    指定路由的重试机制进行关闭

    zuul.routes..retryable=false

注意:route就是你的服务名,比如zuul.routes.hello-service.retryable=false

7.6 异常处理

首先,我们尝试创建一个pre类型的过滤器,并在该过滤器的run方法实现中抛出一个异常。比如下面的实现,在run方法中调用的doSomething方法将抛出Runt imeException异常。

  1. import com.netflix.zuul.ZuulFilter;
  2. import com.netflix.zuul.exception.ZuulException;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.stereotype.Component;
  6. @Component
  7. public class ThrowExceptionFilter extends ZuulFilter {
  8. private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);
  9. @Override
  10. public String filterType() {
  11. return "pre";
  12. }
  13. @Override
  14. public int filterOrder() {
  15. return 0;
  16. }
  17. @Override
  18. public boolean shouldFilter() {
  19. return true;
  20. }
  21. @Override
  22. public Object run() throws ZuulException {
  23. log.info("这是一个pre过滤器,将会抛出异常");
  24. this.doSomething();
  25. return null;
  26. }
  27. private void doSomething() {
  28. throw new RuntimeException("Exist some errors... ");
  29. }
  30. }

在添加了上面的过滤器之后,我们可以将该应用以及之前的相关应用运行起来,并根据网关配置的路由规则访问服务接口,比如http://localhost:5555/api-a/hello此时我们会发现,在API网关服务的控制台中输出了ThrowExceptionFilter的过滤逻辑中的日志信息。
在这里插入图片描述

但是返回给页面的信息中却拿不到我们想打印的信息
在这里插入图片描述

try-catch 处理

在catch异常的处理逻辑中并没有做任何输出操作,而是向请求上下文中添加了一些error相关的参数,主要
有下面三个参数。

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

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

  1. @Component
  2. public class ThrowExceptionFilter extends ZuulFilter {
  3. private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);
  4. @Override
  5. public String filterType() {
  6. return "pre";
  7. }
  8. @Override
  9. public int filterOrder() {
  10. return 0;
  11. }
  12. @Override
  13. public boolean shouldFilter() {
  14. return true;
  15. }
  16. @Override
  17. public Object run() throws ZuulException {
  18. log.info("这是一个pre过滤器,将会抛出异常");
  19. RequestContext ctx = RequestContext.getCurrentContext();
  20. try {
  21. doSomething();
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  25. ctx.set("error.exception", e);
  26. }
  27. return null;
  28. }
  29. private void doSomething() throws Exception {
  30. throw new Exception("Exist some errors... ");
  31. }
  32. }

禁用过滤器

在Zuul中特别提供了-一个参数来禁用指定的过滤器,该参数的配置格式如下:

  1. zuul.<SimpleClassName>.<filterType>.disable=true

比如

  1. zuul.AccessFilter.pre.disable=true

7.7 服务的降级

  1. import com.cloud.apigateway.filter.ThrowExceptionFilter;
  2. import com.netflix.hystrix.exception.HystrixTimeoutException;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.http.HttpHeaders;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.MediaType;
  8. import org.springframework.http.client.ClientHttpResponse;
  9. import org.springframework.stereotype.Component;
  10. import java.io.ByteArrayInputStream;
  11. import java.io.InputStream;
  12. @Component
  13. public class FallBackConfig implements org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider {
  14. private static Logger log = LoggerFactory.getLogger(FallBackConfig.class);
  15. /**
  16. * 定义构造函数
  17. **/
  18. public ClientHttpResponse fallbackResponse() {
  19. return response(HttpStatus.INTERNAL_SERVER_ERROR);
  20. }
  21. @Override
  22. public String getRoute() {
  23. return "*";
  24. }
  25. @Override
  26. public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
  27. // 捕获超时异常,返回自定义信息
  28. if (cause instanceof HystrixTimeoutException) {
  29. return response(HttpStatus.GATEWAY_TIMEOUT);
  30. } else {
  31. return fallbackResponse();
  32. }
  33. }
  34. private ClientHttpResponse response(final HttpStatus status) {
  35. return new ClientHttpResponse() {
  36. @Override
  37. public HttpStatus getStatusCode() {
  38. return status;
  39. }
  40. @Override
  41. public int getRawStatusCode() {
  42. return status.value();
  43. }
  44. @Override
  45. public String getStatusText() {
  46. return status.getReasonPhrase();
  47. }
  48. @Override
  49. public void close() {
  50. log.warn("连接服务关闭,无法进行访问");
  51. }
  52. @Override
  53. public InputStream getBody() {
  54. String message =
  55. "{\n" +
  56. "\"code\": 500,\n" +
  57. "\"message\": \"微服务飞出了地球\"\n" +
  58. "}";
  59. return new ByteArrayInputStream(message.getBytes());
  60. }
  61. @Override
  62. public HttpHeaders getHeaders() {
  63. HttpHeaders headers = new HttpHeaders();
  64. headers.setContentType(MediaType.APPLICATION_JSON);
  65. return headers;
  66. }
  67. };
  68. }
  69. }

在这里插入图片描述

7.8 动态加载

在微服务架构中,由于API网关服务担负着外部访问统一入口的重任,它同其他应用不同,任何关闭应用和重启应用的操作都会使系统对外服务停止,对于很多7×24小时服务的系统来说,这样的情况是绝对不被允许的。所以,作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则、动态添加/删除过滤器等。
通过Zuul实现的API网关服务当然也具备了动态路由和动态过滤器的能力。我们可以在不重启API网关服务的前提下,为其动态修改路由规则和添加或删除过滤器。下面我们分别来看看如何通过Zuul来实现动态API 网关服务。

7.8.1 动态路由

通过之前对请求路由的详细介绍,我们可以发现对于路由规则的控制几乎都可以在配置文件application.properties或 application.yaml中完成。既然这样,对于如何实现Zuul 的动态路由,我们很自然地会将它与Spring Cloud Config 的动态刷新机制联系到一起。只需将API 网关服务的配置文件通过Spring Cloud Config连接的Git仓库存储和管理,我们就能轻松实现动态刷新路由规则的功能。
在介绍如何具体实现.API网关服务的动态路由之前,我们首先需要一个连接到Git仓库的分布式配置中心config-server应用。如果还没有搭建过分布式配置中心的话,建议先阅读第8章的内容《springCloud08—分布式配置中心:Spring Cloud Config》,对分布式配置中心的运作机制有一个基础的了解,并构建一个config-server应用,以配合完成下面的内容。
在具备了分布式配置中心之后,为了方便理解我们重新构建一个API网关服务,该服务的配置中心不再配置于本地工程中,而是从config-server中获取,构建过程如下所示。

  • 创建一个基础的 Spring Boot web工程,命名为api-gateway-dynamic-route。
  • 在pom. xml中引入对zuul、eureka和config 的依赖,具体内容如下:

    <?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“

    1. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    2. <modelVersion>4.0.0</modelVersion>
    3. <parent>
    4. <groupId>org.springframework.boot</groupId>
    5. <artifactId>spring-boot-starter-parent</artifactId>
    6. <version>2.4.8</version>
    7. <relativePath/> <!-- lookup parent from repository -->
    8. </parent>
    9. <groupId>com.cloud</groupId>
    10. <artifactId>api-gateway-dynamic-route</artifactId>
    11. <version>0.0.1-SNAPSHOT</version>
    12. <name>api-gateway-dynamic-route</name>
    13. <description>api-gateway-dynamic-route</description>
    14. <properties>
    15. <java.version>11</java.version>
    16. </properties>
    17. <dependencies>
    18. <dependency>
    19. <groupId>org.springframework.boot</groupId>
    20. <artifactId>spring-boot-starter-web</artifactId>
    21. </dependency>
    22. <dependency>
    23. <groupId>org.springframework.boot</groupId>
    24. <artifactId>spring-boot-starter-test</artifactId>
    25. <scope>test</scope>
    26. </dependency>
    27. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-client -->
    28. <dependency>
    29. <groupId>org.springframework.cloud</groupId>
    30. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    31. </dependency>
    32. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-zuul -->
    33. <dependency>
    34. <groupId>org.springframework.cloud</groupId>
    35. <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    36. <version>2.2.9.RELEASE</version>
    37. </dependency>
  1. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-config -->
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-config</artifactId>
  5. </dependency>
  6. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-bootstrap -->
  7. <dependency>
  8. <groupId>org.springframework.cloud</groupId>
  9. <artifactId>spring-cloud-starter-bootstrap</artifactId>
  10. </dependency>
  11. </dependencies>
  12. <dependencyManagement>
  13. <dependencies>
  14. <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
  15. <dependency>
  16. <groupId>org.springframework.cloud</groupId>
  17. <artifactId>spring-cloud-dependencies</artifactId>
  18. <version>2020.0.2</version>
  19. <type>pom</type>
  20. <scope>import</scope>
  21. </dependency>
  22. </dependencies>
  23. </dependencyManagement>
  24. </project>

创建bootstrap.properties文件

  1. spring.application.name=api-getway
  2. # 注册服务的时候使用服务的ip地址
  3. eureka.instance.prefer-ip-address=true
  4. eureka.client.service-url.defaultZone=http://localhost:1112/eureka/
  5. # 开启通过服务来访问config server功能
  6. spring.cloud.config.discovery.enabled=true
  7. # 通过serviceId来指定config server注册的服务名
  8. spring.cloud.config.discovery.service-id=config-server
  9. #指定的环境
  10. spring.cloud.config.profile=default
  11. #指定分支,当使用git的时候,默认是master
  12. spring.cloud.config.label=master
  13. # 配置项目启动端口号
  14. server.port=5556
  15. # 设置安全访问的用户名和密码
  16. spring.cloud.config.username=root
  17. spring.cloud.config.password=123456
  18. # 设置连接失败的快速响应
  19. spring.cloud.config.fail-fast=true
  20. #actuator配置
  21. management.endpoints.enabled-by-default=true
  22. management.endpoint.health.show-details=always
  23. management.endpoints.web.exposure.include=*
  24. management.endpoints.web.base-path=/actuator

同时在你的git仓库里放入api-getway.properties配置文件
在这里插入图片描述

文件配置如下:

  1. # 路由配置
  2. # 配置hello-service的服务路由
  3. zuul.routes.api-a.path=/api-a/**
  4. zuul.routes.api-a.serviceId=hello-service
  5. # feign-consumer的服务路由
  6. zuul.routes.api-b.path=/api-b/**
  7. zuul.routes.api-b.serviceId=feign-consumer

在这里插入图片描述

创建用来启动API网关的应用主类。这里我们需要使用@RefreshScope注解来将Zuul 的配置内容动态化,具体实现如下:

  1. @SpringBootApplication
  2. @EnableZuulProxy
  3. @EnableDiscoveryClient
  4. public class ApiGatewayDynamicRouteApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(ApiGatewayDynamicRouteApplication.class, args);
  7. }
  8. @Bean
  9. @RefreshScope
  10. @ConfigurationProperties("zuul")
  11. @Primary
  12. public ZuulProperties zuulProperties() {
  13. return new ZuulProperties();
  14. }
  15. }

接下来我们开始访问地址:http://localhost:5556/actuator/routes
在这里插入图片描述

可以看到路由的配置已经正常加载进来
接下来我们把/api-a/改为/api-abcd/
在这里插入图片描述

通过POST请求发送到http://localhost:5556/actuator/refresh
在这里插入图片描述

在这里插入图片描述

可以看到我们已经动态的修改成功
通过本节对动态路由加载内容的介绍,我们可以看到,通过Zuul构建的API网关服务对于动态路由的实现总体上来说还是非常简单的。
美中不足的一点是,Spring Cloud Config并没有UI管理界面,我们不得不通过Git客户端来进行修改和配置,所以在使用的时候并不是特别方便,当然有条件的团队可以自己开发一套UI界面来帮助管理这些路由规则。

7.8.2 动态过滤器

既然通过Zuul构建的 API 网关服务能够轻松地实现动态路由的加载,那么对于API网关服务的另外一大重要功能——请求过滤器的动态加载自然也不能放过,只是对于请求过滤器的动态加载与请求路由的动态加载在实现机制上会有所不同。
其实这个也不难理解,通过之前介绍的请求路由和请求过滤的示例,我们可以看到请求路由通过配置文件就能实现,而请求过滤则都是通过编码实现。所以,对于实现请求过滤器的动态加载,我们需要借助基于JVM实现的动态语言的帮助,比如Groovy。
下面,我们将通过一个简单的示例来演示如何构建一个具备动态加载Groovy过滤器能力的API网关服务的详细步骤。

  • 1.创建一个基础的Spring Boot web工程,命名为api-gateway-dynamic-filter。
  • 2.在pom.xml中引入对 zuul、eureka和 groovy的依赖,具体内容如下:

    <?xml version=”1.0” encoding=”UTF-8”?>


    4.0.0

    org.springframework.boot
    spring-boot-starter-parent
    2.4.8


    com.cloud
    api-gateway-dynamic-filter
    0.0.1-SNAPSHOT
    api-gateway-dynamic-filter
    api-gateway-dynamic-filter

    11



    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-test
    test



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client




    org.springframework.cloud
    spring-cloud-starter-netflix-zuul
    2.2.9.RELEASE



    org.codehaus.groovy
    groovy-all
    3.0.8
    pom




    org.springframework.boot
    spring-boot-configuration-processor
    true





    org.springframework.cloud
    spring-cloud-dependencies
    2020.0.2
    pom
    import




在application.properties添加配置:

  1. server.port=5555
  2. spring.application.name=api-getway
  3. # 注册服务的时候使用服务的ip地址
  4. eureka.instance.prefer-ip-address=true
  5. eureka.client.service-url.defaultZone=http://localhost:1112/eureka/
  6. # 路由配置
  7. # 配置hello-service的服务路由
  8. zuul.routes.api-a.path=/api-a/**
  9. zuul.routes.api-a.serviceId=hello-service
  10. # 增加动态过滤器
  11. #用来指定动态加载的过滤器存储路径;
  12. zuul.filter.root=filter
  13. # 用来配置动态加载的间隔时间,以秒为单位
  14. zuul.filter.interval=5

创建用来加载自定义属性的配置类,命名为 FilterConfiguration,具体内容如下:

  1. import org.springframework.boot.context.properties.ConfigurationProperties;
  2. @ConfigurationProperties("zuul.filter")
  3. public class FilterConfiguration {
  4. private String root;
  5. private Integer interval;
  6. public String getRoot() {
  7. return root;
  8. }
  9. public void setRoot(String root) {
  10. this.root = root;
  11. }
  12. public Integer getInterval() {
  13. return interval;
  14. }
  15. public void setInterval(Integer interval) {
  16. this.interval = interval;
  17. }
  18. }

修改启动类,并在该类中引入上面定义的FilterConfiguration 配置,并创建动态加载过滤器的实例。具体内容如下:

  1. @SpringBootApplication
  2. @EnableConfigurationProperties({
  3. FilterConfiguration.class})
  4. @EnableZuulProxy
  5. public class ApiGatewayDynamicFilterApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(ApiGatewayDynamicFilterApplication.class, args);
  8. }
  9. public FilterLoader filterLoader(FilterConfiguration filterConfiguration) {
  10. FilterLoader filterLoader = FilterLoader.getInstance();
  11. filterLoader.setCompiler(new GroovyCompiler());
  12. try {
  13. FilterFileManager.setFilenameFilter(new GroovyFileFilter());
  14. FilterFileManager.init(filterConfiguration.getInterval(),
  15. filterConfiguration.getRoot() + "/pre",
  16. filterConfiguration.getRoot() + "post");
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. throw new RuntimeException(e);
  20. }
  21. return filterLoader;
  22. }
  23. }

至此,我们就已经完成了为基础的API网关服务增加动态加载过滤器的能力。

根据上面的定义面的定义,API 网关应用会每隔5秒,从API网关服务所在位置的filter/pre和filter/post目录下获取Groovy定义的过滤器,并对其进行编译和动态加载使用。

对于动态加载的时间间隔,可通过zuul.filter.interval参数来修改。

而加载过滤器实现类的根目录可通过zuul.filter.root调整根目录的位置来修改,但是对于根目录的子目录,这里写死了读取/pre和/post目录,实际使用的时候我们可以做进一步扩展。

在完成上述构建之后,我们可以将涉及的服务,比如eureka-server,hello-service以及上述实现的API网关服务都启动起来。

在没有加入任何自定义过滤器的时候,根据路由规则定义,我们可以尝试向API网关服务发起请求:http:// localhost:5555/hello-service/hello,如果配置正确,该请求会被API网关服务路由到hello-service 上,并返回输出Hello world。

下一篇:《springCloud08—分布式配置中心:Spring Cloud Config》

发表评论

表情:
评论列表 (有 0 条评论,111人围观)

还没有评论,来说两句吧...

相关阅读