《Spring Cloud微服务架构实战》--集群网关--Zuul


  Spring Cloud微服务架构实战》--集群网关--Zuul

在前面章节介绍的例子中,我们都是直接访问服务调用者的URL来访问微服务,在实 际环境中,应用程序会有多个服务调用者,如何将它们组织起来,统一对外提供服务呢?

本章将讲述使用Netflix的Zuul框架构建微服务集群的网关。

7.1 Zuul框架介绍

7.1.1 关于 Zuul

 Spring Cloud集群提供了多个组件,用于进行集群内部的通信,例如服务管理组件 Eureka,负载均衡组件Ribbon。如果集群提供了 API或者Web服务,需要与外部进行通信, 比较好的方式是添加一个网关,将集群的服务都隐藏到网关后面。

这种做法对于外部客户 端来说,无须关心集群的内部结构,只需关心网关的配置等信息;对于Spring Cloud集群 来说,不必过多暴露服务,提升了集群的安全性。

代理层作为应用集群的大门,在技术选取上尤为重要,很多传统的解决方案,在软件 上选择了 Nginx、Apache等服务器。Netflix提供了自己的解决方案:Zuul。

Zuul是Netflix 的一个子项目,Spring Cloud将Zuul进行了进一步的实现与封装,将其整合到spring-netflix 项目中,为微服务集群提供代理、过滤、路由等功能。

7.1.2 Zuul 的功能

Zuul将外部的请求过程划分为不同的阶段,每个阶段都提供了一系列过滤器,这些过滤器可以帮助我们实现以下功能:

  • 身份验证和安全性:对需要身份认证的资源进行过滤,拒绝处理不符合身份认证的 请求。
  • 观察和监控:跟踪重要的数据,为我们展示准确的请求状况。
  • 动态路由:将请求动态路由到不同的服务集群。
  • 负载分配:设置每种请求的处理能力,删除那些超出限制的请求。
  • 静态响应处理:提供一些静态的过滤器,直接响应一些请求,而不将它们转发到集 群内部。
  • 路由的多样化:除了可以将请求路由到Spring Cloud集群外,还可以将请求路由到 其他服务。

7.2 在Web项目中使用Zuul

下面初步展示Zuul的路由功能

7.2.1 Web 项目整合 Zuul

新建一个名称为first-router的Maven项目,项目使用的依赖如下:


  org.springframework.cloud
  spring-cloud-starter-zuul
  org.apache.httpcomponents   httpclient   4.5.3

  需要加入spring-cloud-starter-zuul依赖,由于Zuul底层使用了 HttpClient,因此还要加 入相应的依赖。

为了能让Web项目开启对Zuul的支持,在应用类中加入@EnableZuulProxy 注解,请见代码:

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
  public static void main(String[] args) {
    new SpringApplicationBuilder(GatewayApplication.class).properties("server.port=808O'*).run (args);
  }
}

  注意该项目的启动端口为8080。完成以上工作后,一个拥有Zuul功能的Web项目就 建立好了,接下来,将测试它的路由功能。

7.2.2测试路由功能

前一小节已经建立了路由项目,接下来建立源服务的项目,测试示例的结构请见图所示

新建名称为book-server的Maven项目,该项目是一个普通的Spring Boot项目,使用 以下依赖:


  org.springframework.boot 
  spring-boot-starter-web
  l.5.4.RELEASE

  为book-server添加一个/hello服务,项目的启动类以及控制器请见代码:

@SpringBootApplication
@RestController
public class BookApplication {
  @RequestMapping(value = "/hello/(name}", method = RequestMethod.GET) 
  public String hello(@PathVariable String name) {     return "hello " + name;   }   public static void main(String[] args) (     new SpringApplicationBuilder(BookApplication.class).properties("server.port=8090").run(args);
  }
}

为了简单起见,本例将启动类与控制器写到了一起,注意book-server的端口为8090 在控制器中,建立了一个/hello/{name}服务,成功调用后,会返回相应的字符串。

接下来, 修改first-router项目的配置文件,让其进行转发工作

修改first-router项目的application.yml文件,加入以下内容:

zuul:
  routes:
    books:
      url: http://localhost:8090

加入以上配置后,发送给http://localhost:8080/books的所有请求会被转发到8090 口,也就是访问first-router项目,实际上最终会调用book-server的服务。

启动两个应用, 在浏览器中输入地址http://localhost:8080/books/hello/crazyit,可以看到浏览器输出如下:

hello crazyit

  根据输出结果可知,发送的请求已经被转发到book-server进行处理。

7.2.3过滤器运行机制:

在前面的路由项目中,我们使用了@EnableZuulProxy注解。开启该注解后,在Spring 容器初始化时,会将Zuul的相关配置初始化,其中包含一个Spring Boot的Bean: ServletRegistrationBean,该类主要用于注册 Servlet。

Zuul 提供了一个 ZuulServlet 类,在 Servlet的service方法中,执行各种Zuul过滤器(ZuulFilter)。图7-2所示为HTTP请求在 ZuulServlet中的生命周期。

ZuulServlet的service方法接收到请求后,会执行pre阶段的过滤器,再执行routing 段的过滤器,最后执行post阶段的过滤器。其中routing阶段的过滤器会将请求转发到“源 服务”,源服务可以是第三方的Web服务,也可以是Spring Cloud的集群服务。

在执行pre 和routing阶段的过滤器时,如果出现异常,则会执行error过滤器。整个过程的HTTP 求、HTTP响应、状态等数据,都会被封装到一个Requestcontext对象中,这将在后面章节 中讲述。

大致了解了 Zuul的运行机制后,下面开始讲解如何在Spring Cloud中使用Zuul。

7.3 在微服务集群中初试Zuul

在前面小节介绍的例子中,Zuul将请求转发到一个Web项目进行处理,如果实际处理 请求的不是一个Web项目,而是整个微服务集群,那么Zuul将成为整个集群的网关。在 加入Zuul前,Spring Cloud集群的结构请见图

为微服务集群加入Zuul网关后,结构如下图所示:

在深入学习Zuul前,先按上图所示搭建本章的测试项目

7.3.1集群搭建

假设当前需要实现一个书本销售业务,在销售模块中需要调用书本模块的服务来查找 书本,本小节的案例以此为基础,建立以下项目。

  • zuul-eureka-server: Eureka 服务器,应用端口为 8761,源代码目录为 codes\07\03\ zuul-eureka-server。
  • zuul-book-service:书本模块,属于服务提供者,提供/book/{bookld}服务,用于查 找图书,最后返回Book的JSON字符串,应用端口为9000,代码目录为codes\07\ 03\zuul-book-service。
  • zuul-sale-service:销售模块,属于服务调用者,对外发布销售服务 /sale-book/ (bookid}, 在该服务中会调用zuul-book-service来查找Book,应用端口为9100,代码目录为 codes\07\03\zuul-sale-service

书本模块zuul-book-service发布的服务仅返回一个简单的Book对象, 控制器代码如下:

@RequestMapping(value = "/book/{bookId}”,method = RequestMethod.GET, 
          produces = MediaType.APPLICATION_JSON_VALUE) public Book findBook(@PathVariable Integer bookid) {   Book book = new Book();   book.setld(bookid);   book.setName("Workflow 讲义”);   book.setAuthor(”杨恩雄”);   return book; }

销售模块zuul-sale-service发布的服务,相关代码如下:

@FeignClient("zuul-book-service") // 声明调用书本服务
public interface BookService {
  /**
   * 调用书本服务的接口,获取一个Book实例
   */
  @RequestMapping(method = RequestMethod.GET, value = "/book/{bookId}") 
  Book getBook(@PathVariable("bookId") Integer bookId); }
@RestController public class SaleController {   @Autowired   private BookService bookService;
  @RequestMapping(value = "/sale-book/(bookId}", method = RequestMethod.GET)
  public String saleBook(@PathVariable Integer bookld) {     //调用book服务査找     Book book = bookService.getBook(bookld);     //控制台输入,模拟进行销售     Systern.out.printIn("销售模块处理销售,要销售的图书id: "+ book.getld()+ 书名:"+ book.getName());     //销售成功     return "SUCCESS";   } }

  销售模块的服务使用Feign调用书本模块的服务来获取Book实例,然后在控制台中输 岀信息。在实际应用中,销售的过程会更为复杂,例如有可能涉及支付等内容,本例为了 简单起见,仅进行简单的输出。

接下来,创建网关项目。

7.3.2路由到集群服务

在前一小节的基础上,新建一个名称为zuul-gateway的Maven项目(代码目录为codes\07\03\zuul-gateway), pom.xml 中加入以下依赖:


  org.springframework.cloud
  spring-cloud-starter-config


  org.springframework.cloud 
  spring-cloud-starter-eureka
  org.springframework.cloud
  spring-cloud-starter-zuul
  org.apache.httpcomponents   httpclient   4.5.3

新建应用类,如代码所示:

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
  public static void main(String[] args) (
    new SpringApplicationBuilder(GatewayApplication.class).properties("server.port=8080").run(args);
  }
}

应用类跟前面的例子一致,使用@EnableZuulProxy注解。但是,由于网关项目需要加到集群中,因此要修改配置文件,让其注册到Eureka服务器中。

本例的配置文件如代所示:

spring:
  application:
    name: zuul-gateway
  eureka:
    instance:
      hostname: localhost
    client:
      serviceUrl:
        defaultzone: http://localhost:8761/eureka/
  zuul:
    routes:
      sale:
        path: /sale/**
        serviceld: zuul-sale-service

使用eureka的配置,将自己注册到8761的Eureka中。在配置Zuul时,声明所有的/sale/** 请求将会被转发到Id为zuul-sale-service的服务进行处理。

一般情况下,配置了 serviceld后,在处理请求的routing阶段,将会使用一个名称为 RibbonRoutingFiIter的过滤器,该过滤器会调用Ribbon的API来实现负载均衡,默认情况 下用HttpClient来调用集群服务。

按照以下顺序启动集群:

  • 启动 zuul-eureka-server(Eureka 服务器)
  • 启动zuul-book-service(服务提供者)
  • 启动zuul-sale-service(服务调用者)
  • 启动zuul-gateway(集群网关)

在浏览器中访问 http://localhost:8080/sale/sale-booVl,返回 SUCCESS 字符串,在销售 模块的控制台,可以看到输出如下:

销售模块处理销售,要销售的图书id: 1,书名:Workflow讲义

根据输出可知,销售模块、书本模块均被调用。本例涉及4个项目,下图展示了本例的结构,可帮助读者理解本例。

7.3.3 ZuulHttp 客户端

  我们知道,Ribbon用来实现负载均衡,Ribbon在选取了合适的服务器后,再调用REST客户端API来调用集群服务。

在默认情况下,将使用HttpClient的API来调用集群服务。除了 HttpClient 外,还可以使用 OkHttpClient, 以及 com.netflix.niws.client.http.RestClient,  RestClient目前己经不推荐使用,

如果想启用OkHttpClient,可以添加以下配置:

 

 ribbon.okhttp.enabled=true

 

  除了该配置外,还要在 pom.xml 中加入 OkHttpClient 的依赖:


  com.squareup.okhttp3
  okhttp

7.4 路由配置

路由配置看似简单,但也有部分规则需要说明,本节以7.3节搭建的集群项目为基础 讲解Zuul的路由配置。

7.4.1简单路由

  Spring Cloud在Zuul的routing阶段实现了几个过滤器,这些过滤器决定如何进行路由 工作。其中,最基本的就是SimpleHostRoutingFilter, 该过滤器运行后,会将HTTP请求全 部转发到“源服务”(HTTP服务),

本书将其称为简单路由,本章7.2节的例子实际上就是 使用了简单路由进行请求转发。

以下为简单路由的配置,同时使用了 path与url:

zuul:
  routes:
    routeTest:
      path: /routeTest/163
      url: http://www.163.com

  以上的配置访问http://localhost:8080/reuteTest/l63,将会跳转到163网站。

为了配置简 便,可以省略path,默认情况下使用routeld作为path,以下的配置省略了 path配置:

zuul:
  routes:
    routel63:
      url: http://www.163.com

访问http://localhost:8080/route 163,同样会路由到163网站。实际上,要触发简单路由, 配置的url的值需要以http:或者https:字符串开头。

以下的配置不能触发简单路由:

zuul:
  routes:
    noRoutel63:
      url: www.163.com

简单路由的过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将 HttpServletRequest的相关数据(HTTP方法、参数、请求头等)转换为HttpClient的请求实HttpRequest),再使用 CloseableHttpClient 进行转发。

在此过程中,为了保证转发的性能,使用了 HttpClient的连接池功能。

涉及连接池, 就需要对其进行配置。

在使用简单路由时,可以配置以下两项,修改HttpClient连接池的属性。

  • zuul.host.maxTotalConnections:目标主机的最大连接数,默认值为200。配置该项,相当于调用 PoolingHttpClientConnectionManager setMaxTotal 方法。
  • zuul.host.maxPerRouteConnections:每个主机的初始连接数,默认值为20。配置该项,相当于调用了 PoolingHttpClientConnectionManager setDefaultMaxPerRoute 方法。

7.4.2 跳转路由

除了简单路由外,也支持跳转路由。当外部访问网关的A地址时,会跳转到B地址, 处理跳转路由的过滤器为SendForwardFilter。

接下来进行简单测试,为网关项目 (zuul-gateway)添加一个控制器,请见代码:

@RestController
public class Sourcecontroller {
  @RequestMapping(value = "/source/hello/(name}", method = RequestMethod.GET) 
  public String hello(@PathVariable("name") String name) {     return "hello " + name;   } }

  控制器中提供了一个最简单的hello服务,用来当作"源服务”,在application.yml 进行转发配置,配置项如下:

zuul:
  routes:
    helioRoute:
      path: /test/**
      url: forward:/source/hello

当外部访问/test地址时,将会自动跳转到/source/hello地址。打开浏览器,输入http:// localhost:8080/test/anugs,可以看到浏览器会返回字符串hello angus,可见源服务被调用。

跳转路由实现较为简单,实际上是调用了 RequestDispatcher的forward方法进行跳转。

7.4.3 Ribbon 路由

在7.3.2节中,我们己经接触过Ribbon路由。当网关作为Eureka客户端注册到Eureka 服务器时,可以通过配置serviceld将请求转发到集群的服务中。

使用以下配置,可以执行 Ribbon路由过滤器:

zuul:
  routes:
    sale:
      path: /sale/**
      serviceld: zuul-sale-service

与简单路由类似,serviceld也可以被省略。当省略时,将会使用routeld作为serviceld, 下面的配置片断,效果等同于上面的配置:

zuul:
  routes:
    zuul-sale-service: 
      path: /sale/**

需要注意的是,如果提供的url配置项不是简单路由格式(不以http:或https:开), 也不是跳转路由格式(fbrward:开头),

那么将会执行Ribbon路由过滤器,将url看作一个 serviceldo下面的配置片断,效果也等同于前面的配置:

zuul:
  routes:
    sale:
      path: /sale/**
      url: zuul-sale-service

7.4.4自定义路由规则

如果上面的路由配置无法满足实际需求,可以考虑使用自定义的路由规则。实现方式 较为简单,在配置类中创建一个PattemServiceRouteMapper即可,请见代码

@Configuration
public class MyConfig {
  @Bean
  public PattemServiceRouteMapper patternServiceRouteMapper () { 
    return new PattemServiceRouteMapper("(zuul)-(? +)-(service)", "${module}/**");   } }

创建了 PattemServiceRouteMapper实例,构造器的第一个参数为serviceld的正则表达式,第二个参数为路由的patho访问module/**的请求,将会被路由到zuul-module-service 的微服务。

更进一步,以上的路由规则,如果想让一个或多个服务不被路由,可以使用 zuuLignoredServices属性。

例如在代码清单7-6的基础上,想排除zuul-sale-service、 zuul-book-service 这两个模块,可以配置 zuuLignoredServices: zuul-sale-service, zuul-book- service

7.4.5 忽略路由

除了上面提到的zuuLignoredServices配置可以忽略路由外,还可以使用 zuul.ignoredPattems来设置不进行路由的URL,请见以下配置片断:

zuul:
  ignoredPatterns: /sale/noRoute
  routes:
    sale:
      path: /sale/**
      serviceld: zuul-sale-service

访问/sale路径的请求都会被路由到zuul-sale-service进行处理,但/sale/noRoute除外。

7.5 Zuul的其他配置

本节将讲解Zuul 一些较为常用的配置。

7.5.1请求头配置

  在集群的服务间共享请求头并没有什么问题,但是如果请求会被转发到其他系统,那 么对于敏感的请求头信息,就需要进行处理。

在默认情况下,HTTP请求头的Cookie Set-Cookie、 Authorization属性不会传递到"源服务",可以使用sensitiveHeaders属性来配置敏感请求头,

下面的配置对全局生效:

zuul:
  sensitiveHeaders: accept-language, cookie

以下的配置片断,仅对一个路由生效:

zuul:
  routes:
    sale:
      path: /sale/**
      serviceld: zuul-sale-service
      sensitiveHeaders: cookie

除了使用sensitiveHeaders属性外,还可以使用ignoredHeaders属性来配置全局忽略的 请求头。使用该配置项后,请求与响应中所配置的头信息均被忽略:

zuul:
  ignoredHeaders: accept-language

7.5.2路由端点

在网关项目中提供了一个/routes服务,可以让我们查看路由映射信息。如果想开启该 服务需要满足以下条件:

  • 网关项目中引入了 Spring Boot Actuatoro
  • 项目中使用了@EnableZuulProxy注解。

一般情况下,Actuator开启了端点的安全认证,即使符合以上两个条件,也无法访问 routes服务。要解决该问题,可以在配置文件中将management.security.enabled属性值设置 为false关闭安全认证。

以7.3节中介绍的项目为例,开启/routes服务后访问http://localhost:8080/routes,浏览 器中将输出以下JSON (以下JSON经过格式化):

{
  "/sale/**": "zuul-sale-service",
  "/routeTest/163": "http://www.163.com",
  "/routel63/**": "http://www.163.com",
  "/noRoutel63/**": "www.163.com",
  "/test/**": "forward:/source/hello",
  "/zuul-sale-service/**": "zuul-sale-service",
  "/zuul-book-service/**": "zuul-book-service"
}

7.5.3 Zuul Hystrix

当我们对网关进行配置让其调用集群的服务时,将会执行Ribbon路由过滤器 (RibbonRoutingFilter);

该过滤器在进行转发时会封装为一个Hystrix命令予以执行。

换言之,它具有容错的功能。如果“源服务”出现问题(例如超时),那么所执行的Hystrix令将会触发回退,下面将测试Zuul中的回退。

为销售模块(zuul-sale-service)的控制器添加一个超时方法,请见代码

@RequestMapping(value = "/errorTest", method = RequestMethod.GET)
public String errorTest() throws Exception {
  Thread.sleep(3000);
  return "errorTest";
}

在网关项目(zuul-gateway)中建立一个网关处理类,处理回退逻辑,请见代码:

public class MyFallbackProvider implements ZuulFallbackProvider {
  //返回路由的名称
  public String getRoute() {
    return "zuul-sale-service";
  }
  //回退触发时,返回默认的响应
  public ClientHttpResponse fallbackResponse() {
    return new ClientHttpResponse() {
      public Inputstream getBody() throws lOException {
        return new ByteArraylnputStream("fallback".getBytes());
      }
      public HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.TEXT_PLA工N);
        return headers;
      }
      public HttpStatus getStatusCode() throws lOException {
        return HttpStatus.OK;
      }
      public int getRawStatusCode() throws lOException {         return 200;       }       public String getStatusText() throws lOException {         return "OK";       }       public void close() {       }     };   } }

回退处理类需要实现ZuulFallbackProvider接口,实现的getRoute方法返回路由的名称, 该方法将与配置中的路由进行对应,本例配置的路由如下:

zuul:
  routes:
    sale:
      path: /sale/**
      serviceld: zuul-sale-service

简单点说就是,zuul-sale-service路由出现问题导致触发回退时,由MyFallbackProvider 理。MyFallbackProvider 类实现的 fallbackResponse 方法要返回一个 ClientHttpResponse 实例。

本例中返回的ClientHttpResponse,内容为fallback,也就是回退触发时,调用的客户端将 得到fallback字符串。

为了让Spring 容器知道 MyFallbackProvider,在配置类中新建 MyFallbackProvider Bean,如代码:

@Configuration
public class FallbackConfig {
  @Bean
  public ZuulFallbackProvider saleFallbackProvider() {
    return new MyFallbackProvider();
  }
}

启动整个集群,在浏览器中访问以下地址http://localhost:8080/sale/errorTest,浏览器返 回fallback字符串,可见回退被触发。

以上实现的MyFalIbackProvider仅对zuul-sale-service路由有效,如果想对全局有效, 可以使用以下实现:

public String getRoute() {
  return "*";
}

7.5.4 Zuul 中预加载 Ribbon

调用集群服务时,会使用Ribbon的客户端。默认情况下,客户端相关的Bean会延迟加载,在第一次调用集群服务时,才会初始化这些对象。

在第一次调用时,控制台会有以 下的输出日志(仅截取部分):

2017-08-28 18:44:31.963 INFO 528--[main]
c.n.1.DynamicServerListLoadBalancer :
DynamicServerListLoadBalancer for client zuul-sale-service initialized:

如果想提前加载Ribbon客户端,可以在配置文件中进行以下配置:

zuul:
  ribbon:
    eager-load:
      enabled: true

以上的配置在Spring容器初始化时,就会创建Ribbon客户端的相关实例。启动网关项 目可以看到以上的输出日志。

至此,Zuul的基本功能已经介绍完毕。掌握了前面章节介绍的内容,基本上就可以使 用Zuul 了。接下来,再进一层,让我们更深入地学习Zuul。

7.6 Zuul功能进阶

7.6.1过滤器优先级

Spring Cloud为HTTP请求的各个阶段提供了多个过滤器,这些过滤器的执行顺序由它们各自提供的一个int值决定,提供的值越小,优先级越高。

图7-6展示了默认的过滤器, 以及它们的优先级。

如图7-6所示,在routing阶段会优先执行Ribbon路由的过滤器,再执行简单路由过 滤器。

7.6.2自定义过滤器

了解过滤器的执行顺序后,我们编写一个自定义过滤器。新建过滤类,继承ZuulFilter, 实现请见代码:

public class MyFilter extends ZuulFilter {
  //过滤器执行条件
  public boolean shouldFilter() {
    return true;   }   //执行方法   public Object run () {     System.out.printin("执行 MyFilter 过滤器");     return null;
  }   //表示将在路由阶段执行
  public String filterType() {
    return Filterconstants.ROUTE_TYPE;   }   //返回1,路由阶段,该过滤将会最先执行   public int filterOrder() {     return 1;   } }

新建的自定义过滤器将会在routing阶段执行,优先级为1,也就是在routing阶段,该 过滤器最先执行。另外注意shouldFilter方法,过滤最终是否执行由该方法决定,本例返回 true,表示访问任何路由规则都会执行该过滤器。

为了让Spring容器知道过滤器的存在, 需要对该类进行配置,代码清单7-11所示为配置类。

@Configuration
public class FilterConfig {
  @Bean
  public MyFilter myFilter() { 
    return new MyFilter();   } }

启动集群,访问网关http://localhost:8080/test/l,会看到控制输出:执行MyFilter过滤 器。

实际上,访问任何一个配置好的路由都会进行输出。

7.6.3动态加载过滤器

相对于集群中的其他节点,网关更需要长期、稳定地提供服务。如果需要增加过滤器, 重启网关代价太大,为了解决该问题,Zuul提供了过滤器的动态加载功能。

可以使用Groovy 来编写过滤器,然后添加到加载目录,让Zuul去动态加载。先为网关项目加入Groovy 依赖:


  org.codehaus.groovy
  groovy-all
  2.4.12

接下来,在网关项目的应用类中,调用Zuul的API来实现动态加载,请见代码:

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
  @PostConstruct   public void zuulInit() {     FilterLoader.getlnstance().setcompiler(new GroovyCompiler());     //读取配置,获取脚本根目录     String scriptRoot = System.getProperty("zuul.filter.root" , "groovy/filters");     //获取刷新间隔     String refreshinterval = System.getProperty("zuul.filter.refreshinterval", "5");
    if (scriptRoot.length () > 0) {
      scriptRoot = scriptRoot + File.separator;
      try (         FilterFileManager.setFilenameFilter(new GroovyFileFilter());         FilterFileManager.init(Integer.parselnt(refreshinterval), scriptRoot+"pre", scriptRoot + "route", scriptRoot + "post");       } catch (Exception e) {         throw new RuntimeException(e);       }
    }
  }
  public static void main(String[] args) {     new SpringApplicationBuiIder(GatewayApplication.class).properties("server.port=8080").run(args);   } }

在启动类中,增加了 zuullnit方法,使用@PostConstruct进行修饰。在该方法中,先读取zuul.filter.root和zuul.filter.refreshlnterval两个属性,分别表示动态过滤器的根目录以及 刷新间隔,刷新间隔以秒为单位。

这两个属性优先读取配置文件的值,如果没有则使用默认值。在配置文件中,可使用下面的配置片断:

zuul:
  filter:
    root: "groovy/filters"
    refreshinterval: 5

调用FilterFileManager的init方法,初始化3个过滤器目录:pre、route和posto为了 测试动态加载,使用Groovy编写一个最简单的过滤器,请见代码:

class DynamicFilter extends ZuulFilter {
  public boolean shouldFilter() {
    return true;
  }
  public Object run() {
    System. out. printin ("=========这一个是动态加载的过滤器:DynamicFilter"); 
    return null;   }   public String filterType() {     return Filterconstants.ROUTE_TYPE;   }   public int filterOrder() {     return 3;   } }

与前面的过滤器一致,同样继承自ZuulFilter。需要注意的是,本例的过滤器并没有一 开始就放到动态加载的过滤器目录中,读者在测试时,需要先启动网关项目,再将 Dynamic F i 1 ter. groovy放到对应目录中。

完成以上工作后,启动网关项目,访问以下地址http://localhost:8080/test/crazyit,控制 台中并没有输出 DynamicFilter 的信息。

DynamicFilter.groovy 复制到 src/main/java/groovy/ filters/route目录,等待几秒后,重新访问以上地址,可以看到网关的控制台输出如下:

=========这一个是动态加载的过滤器:DynamicFilter

7.6.4 禁用过滤器

如果想禁用其中一个过滤器,可以使用以下配置:

zuul:
  SendForwardFilter:
    route:
      disable: true

以上配置会将SendForwardFilter (处理跳转路由的过滤器)禁用,如果再为url属性使fbrward:进行配置的话,将不会产生跳转效果。同样,禁同其他过滤器也会导致失去相 应的功能。

7.6.5 请求上下文

HTTP请求的全部信息都封装在_个RequestContext对象中,该对象继承ConcurrentHashMap? 可将RequestContext看作一个Map, RequestContext维护着当前线程的全部请求变量,例如 请求的URI、serviceld、主机等信息。

本小节将以RequestContext为基础,编写一个自定 义的过滤器,使用RestTemplate来调用集群服务。

新建一个过滤器,实现请见代码:

public class RestTemplateFiIter extends ZuulFilter {
  private RestTemplate restTemplate;
  public RestTemplateFilter(RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
  }
  public boolean shouldFilter(){
    RequestContext ctx = RequestContext.getCurrentContext(); 
    HttpServletRequest request = ctx.getRequest();     //获取请求uri     String uri = request.getRequestURI();     //为了不影响其他路由,uri中含有rest-tpl-sale才执行本路由器     if(uri.indexOf("rest-tpl-sale") != -1) {       return true;     } else (       return false;
    }
  }
  public Object run() {     Requestcontext ctx = Requestcontext.getCurrentContext();     //获取需要调用的服务id     String serviceld = (String) ctx.get("serviceld");     //获取请求的uri     String uri = (String)ctx.get("requestURI");     //组合成ur丄给RestTemplate调用     String url = "http://" + serviceId + uri;     System.out.printin("执行 RestTemplateFiIter, 调用的 url:" + url);     //调用并获取结果     String result = this.restTemplate.getForObject(url, String.class);     //设置路由状态,表示已经进行路由     ctx.setResponseBody(result);     //设置响应标识     ctx.sendZuulResponse();
    return null;   }
  @Override   public String filterType() {     return Filterconstants.ROUTE_TYPE;   }
  @Override   public int filterOrder() {     return 2;   } }

  RestTemplateFilter的主要功能是使用RestTemplate来调用集群服务。过滤器中的 shouldFilter 方法从 RequestContext 中获取 HttpServletRequest,再得到请求的 uri,如果 uri 含有rest-tpl-sale字符串,才执行本过滤器,

这样做是为了避免影响其他例子的运行效果。

  RestTemplateFilter实现的filterType方法表示该过滤器将在routing阶段执行,执行顺序为2,也就是比Spring Cloud自带的过滤器(routing阶段)都要优先执行。

在RestTemplateFiIter的执行方法中,从RequestContext中获取了 serviceld以及请求的 uri ,再组合成一个url给RestTemplate执行,执行返回的结果被设置到RequestContext中。

  需要注意的是,最后调用了 RequestContext的sendZuulResponse方法来设置响应标识。

调用了该方法后,Spring Cloud自带的Ribbon路由过滤器(RibbonRoutingFilter k简单 路由过滤器(SimpleHostRoutingFilter)将不会执行。

将RestTemplateFilter加入配置中,请见代码:

public class FilterConfig {
  @Bean
  public RestTemplateFilter restTemplateFilter(RestTemplate restTemplate) { 
    return new RestTemplateFilter(restTemplate); // 注入 RestTemplate   }
  @LoadBalanced   @Bean   public RestTemplate getRestTemplate() {     return new RestTemplate();   } }

在application.yml文件中,建立对应的路由规则,请见以下配置片断:

zuul:
  routes:
    restTestRoute:
      path: /rest-tpl-sale/**
      serviceld: zuul-sale-service

以上配置片断,设置路由的path为/rest.tpl.sale,当访问该地址时,将会执行前面的 RestTemplateFilter; 

启动集群,访问以下地址:http://localhost:8080/rest-tpl-sale/sale-book/1, 浏览器输出返回的字符串SUCCESS,控制台输出如下:

执行RestTemplateFilter, 调用的 url: http: //zuul-sale-service/sale-book/1

根据结果可知,我们自定义的过滤器将请求路由到集群的zuul-sale-service服务。本例的作用,除了再次展示如何编写过滤器之外,主要还想让大家了解Requestcontext所维护 的相关信息。

7.6.6 @EnableZuulServer 注解

在本章前面的网关项目中,使用了@EnableZuulProxy来开启Zuul的功能。除了该注解外,还可以使用@EnableZuulServer, 该注解更像一个“低配版”的@EnableZuulProxy。

使用@EnableZuulServer 后,SimpleHostRoutingFilter、RibbonRoutingFilter 等过滤器将不会被启用, 下图展示了使用@EnableZuulServer注解后各阶段的过滤器;

如图 7-7 所示,使用@EnableZuulServer 后,pre 阶段的 PreDecorationFilter, routing 段的RibbonRoutingFilter和SimpleHostRoutingFilter将不会启用。

换言之,默认情况下Zuul 不具备调用集群服务的能力,也不具备简单路由的功能。

如果在实际项目中不希望使用 Spring Cloud RibbonRoutingFilter SimpleHostRoutingFilter, 而想像7.6.4节那样,自己编写过滤器来调用服务,可以考虑使用@EnableZuulServer注解。

7.6.7 error 过滤器

   各阶段的过滤器执行时,抛出的异常会被捕获,然后调用Requestcontext setThrowable方法设置异常。error阶段的SendErrorFilter过滤器会判断RequestContext中是 否存在异常(getThrowable是否为null),

如果存在,才会执行SendErrorFilter过滤器。

  SendErrorFilter过滤器在执行时,会将异常信息设置到HttpServletRequest中,再调用 RequestDispatcher的forward方法,默认跳转到/error页面。

代码清单7-16编写了一个自定 义的过滤器,该过滤器会抛出异常。

public class ExceptionFilter extends ZuulFilter {
  public boolean shouldFilter() {
    Requestcontext ctx = Requestcontext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();
    //获取请求的uri
    String uri = request.getRequestURI();
    //为不影响其他例子,uri含有exceptionTest才执行本过滤器
    if(uri.indexOf("exceptionTest") != -1) (
      return true;
    }
    return false;
  }
  public Object run() {     System.out.printin("ExceptionFilter,将抛出异常");     throw new ZuulRuntimeException(new ZuulException("exception msg", 201, "my cause"));   }
  public String filterType() {     return FilterConstants.ROUTE_TYPE;   }
  public int filterOrder() {     return 3;   } }

在ExceptionFilter的shouldFilter方法中,遇到exceptionTest的uri才会执行,目的是不影响本章其他例子的执行。

在run方法中,简单进行控制台打印,再抛出一个ZuulRuntimeException, 该异常实例包装了一个ZuulExceptiono为了查看异常输出的信息,新建一个控制器,主要在控制台中输出这些信息。

请见代码:

@Controller
public class MyErrorController extends BasicErrorController {
  public MyErrorController(ErrorAttributes errorAttributes) {
    super(errorAttributes, new ErrorProperties());
  }   @Override   public ModelAndView errorHtml(HttpServletRequest request,HttpServletResponse response) {     System.out.printIn("===输出异常信息===”);     System.out.printin(request.getAttribute("javax.servlet.error.status_code"));
    System.out.printin(request.getAttribute("javax.servlet.error.exception"));
    System.out.printin(request.getAttribute("javax.servlet.error.message"));
    return super.errorHtml(request, response);   } }

  MyErrorController 继承了 BasicErrorController, BasicErrorController Spring Boot 中用 于处理错误的控制器基类。在过滤器抛出异常后,SendErrorFilter会跳转到/error路径,然 后就会执行MyErrorController的errorHtml方法返回到错误页面。

在本例中我们不进行处理, 只在方法体中输出此处得到的异常信息。

启动整个集群,访问以下地址http://localhost:8080/ exceptionTest/test,可以看到网关项目的控制台输出如下:

执行ExceptionFilter,将抛出异常
2017-08-29 13:25:34.695 WARN 5956[nio-8080-exec-l] o.s.c.n.z.filters.
post.SendErrorFilter : Error during filtering
……省略中间的异常信息
===输出异常信息 ===
201
com.netflix.zuul.exception.ZuulException: exception msg
my cause

根据输出结果可知,过滤器抛出的异常信息可以在错误处理的控制器中获取。

 7.6.8动态路由

在前面章节中,所有的路由规则都在application.yml中进行配置,在实际应用中, 能一个模块就有一份路由配置文件,而且这些配置文件的内容都在不停变化。如果因为部分变化而重启网关,这是无法想象的;

因此,路由规则的动态刷新功能在实际应用中非常 重要。

路由的动态刷新需要以配置文件的更新、配置项的刷新为前提,这部分内容将在Spring Cloud Config章节中讲解,因此动态路由的实现,也在那一章中讲解,本章不进行讲述。

7.7本章小结

本章以Zuul框架为核心,讲解了Spring Cloud集群中网关的功能。主要演示了在Web 项目、在Spring Cloud中使用Zuul,请求转发、微服务调用等内容较为重要,不仅要学会 如何使用,最好还要知道其实现原理。

本章的7.4节与7.5节介绍了 Zuul的常用配置,握这些配置后,基本就可以使用Zuul 了。

7.6节讲解了过滤器的相关内容,学习该节后, 可以清楚地了解过滤器的工作机制,以便在实际环境中实现自己所需要的功能。