在前面章节介绍的例子中,我们都是直接访问服务调用者的URL来访问微服务,在实 际环境中,应用程序会有多个服务调用者,如何将它们组织起来,统一对外提供服务呢?
本章将讲述使用Netflix的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将外部的请求过程划分为不同的阶段,每个阶段都提供了一系列过滤器,这些过滤器可以帮助我们实现以下功能:
下面初步展示Zuul的路由功能
7.2.1 Web 项目整合 Zuul
新建一个名称为first-router的Maven项目,项目使用的依赖如下:
1 2 3 4 5 6 7 8 |
|
需要加入spring-cloud-starter-zuul依赖,由于Zuul底层使用了 HttpClient,因此还要加 入相应的依赖。
为了能让Web项目开启对Zuul的支持,在应用类中加入@EnableZuulProxy 注解,请见代码:
1 2 3 4 5 6 7 |
|
注意该项目的启动端口为8080。完成以上工作后,一个拥有Zuul功能的Web项目就 建立好了,接下来,将测试它的路由功能。
7.2.2测试路由功能
前一小节已经建立了路由项目,接下来建立源服务的项目,测试示例的结构请见图所示
新建名称为book-server的Maven项目,该项目是一个普通的Spring Boot项目,使用 以下依赖:
1 2 3 |
|
为book-server添加一个/hello服务,项目的启动类以及控制器请见代码:
1 2 3 4 5 6 7 8 |
|
为了简单起见,本例将启动类与控制器写到了一起,注意book-server的端口为8090。 在控制器中,建立了一个/hello/{name}服务,成功调用后,会返回相应的字符串。
接下来, 修改first-router项目的配置文件,让其进行转发工作
修改first-router项目的application.yml文件,加入以下内容:
1 2 3 4 |
|
加入以上配置后,发送给http://localhost:8080/books的所有请求会被转发到8090端 口,也就是访问first-router项目,实际上最终会调用book-server的服务。
启动两个应用, 在浏览器中输入地址http://localhost:8080/books/hello/crazyit,可以看到浏览器输出如下:
1 |
|
根据输出结果可知,发送的请求已经被转发到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。
在前面小节介绍的例子中,Zuul将请求转发到一个Web项目进行处理,如果实际处理 请求的不是一个Web项目,而是整个微服务集群,那么Zuul将成为整个集群的网关。在 加入Zuul前,Spring Cloud集群的结构请见图
为微服务集群加入Zuul网关后,结构如下图所示:
在深入学习Zuul前,先按上图所示搭建本章的测试项目
7.3.1集群搭建
假设当前需要实现一个书本销售业务,在销售模块中需要调用书本模块的服务来查找 书本,本小节的案例以此为基础,建立以下项目。
书本模块zuul-book-service发布的服务仅返回一个简单的Book对象, 控制器代码如下:
1 2 3 4 5 6 7 8 |
|
销售模块zuul-sale-service发布的服务,相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
销售模块的服务使用Feign调用书本模块的服务来获取Book实例,然后在控制台中输 岀信息。在实际应用中,销售的过程会更为复杂,例如有可能涉及支付等内容,本例为了 简单起见,仅进行简单的输出。
接下来,创建网关项目。
7.3.2路由到集群服务
在前一小节的基础上,新建一个名称为zuul-gateway的Maven项目(代码目录为codes\07\03\zuul-gateway),在 pom.xml 中加入以下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
新建应用类,如代码所示:
1 2 3 4 5 6 7 |
|
应用类跟前面的例子一致,使用@EnableZuulProxy注解。但是,由于网关项目需要加到集群中,因此要修改配置文件,让其注册到Eureka服务器中。
本例的配置文件如代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
使用eureka的配置,将自己注册到8761的Eureka中。在配置Zuul时,声明所有的/sale/** 请求将会被转发到Id为zuul-sale-service的服务进行处理。
一般情况下,配置了 serviceld后,在处理请求的routing阶段,将会使用一个名称为 RibbonRoutingFiIter的过滤器,该过滤器会调用Ribbon的API来实现负载均衡,默认情况 下用HttpClient来调用集群服务。
按照以下顺序启动集群:
在浏览器中访问 http://localhost:8080/sale/sale-booVl,返回 SUCCESS 字符串,在销售 模块的控制台,可以看到输出如下:
1 |
|
根据输出可知,销售模块、书本模块均被调用。本例涉及4个项目,下图展示了本例的结构,可帮助读者理解本例。
7.3.3 ZuulHttp 客户端
我们知道,Ribbon用来实现负载均衡,Ribbon在选取了合适的服务器后,再调用REST客户端API来调用集群服务。
在默认情况下,将使用HttpClient的API来调用集群服务。除了 HttpClient 外,还可以使用 OkHttpClient, 以及com.netflix.niws.client.http.RestClient, RestClient目前己经不推荐使用,
如果想启用OkHttpClient,可以添加以下配置:
1 |
|
除了该配置外,还要在 pom.xml 中加入 OkHttpClient 的依赖:
1 2 3 4 |
|
路由配置看似简单,但也有部分规则需要说明,本节以7.3节搭建的集群项目为基础 讲解Zuul的路由配置。
7.4.1简单路由
Spring Cloud在Zuul的routing阶段实现了几个过滤器,这些过滤器决定如何进行路由 工作。其中,最基本的就是SimpleHostRoutingFilter, 该过滤器运行后,会将HTTP请求全 部转发到“源服务”(HTTP服务),
本书将其称为简单路由,本章7.2节的例子实际上就是 使用了简单路由进行请求转发。
以下为简单路由的配置,同时使用了 path与url:
1 2 3 4 5 |
|
以上的配置访问http://localhost:8080/reuteTest/l63,将会跳转到163网站。
为了配置简 便,可以省略path,默认情况下使用routeld作为path,以下的配置省略了 path配置:
1 2 3 4 |
|
访问http://localhost:8080/route 163,同样会路由到163网站。实际上,要触发简单路由, 配置的url的值需要以http:或者https:字符串开头。
以下的配置不能触发简单路由:
1 2 3 4 |
|
简单路由的过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将 HttpServletRequest的相关数据(HTTP方法、参数、请求头等)转换为HttpClient的请求实 例(HttpRequest),再使用 CloseableHttpClient 进行转发。
在此过程中,为了保证转发的性能,使用了 HttpClient的连接池功能。
涉及连接池, 就需要对其进行配置。
在使用简单路由时,可以配置以下两项,修改HttpClient连接池的属性。
7.4.2 跳转路由
除了简单路由外,也支持跳转路由。当外部访问网关的A地址时,会跳转到B地址, 处理跳转路由的过滤器为SendForwardFilter。
接下来进行简单测试,为网关项目 (zuul-gateway)添加一个控制器,请见代码:
1 2 3 4 5 6 |
|
控制器中提供了一个最简单的hello服务,用来当作"源服务”,在application.yml 中进行转发配置,配置项如下:
1 2 3 4 5 |
|
当外部访问/test地址时,将会自动跳转到/source/hello地址。打开浏览器,输入http:// localhost:8080/test/anugs,可以看到浏览器会返回字符串hello angus,可见源服务被调用。
跳转路由实现较为简单,实际上是调用了 RequestDispatcher的forward方法进行跳转。
7.4.3 Ribbon 路由
在7.3.2节中,我们己经接触过Ribbon路由。当网关作为Eureka客户端注册到Eureka 服务器时,可以通过配置serviceld将请求转发到集群的服务中。
使用以下配置,可以执行 Ribbon路由过滤器:
1 2 3 4 |
|
与简单路由类似,serviceld也可以被省略。当省略时,将会使用routeld作为serviceld, 下面的配置片断,效果等同于上面的配置:
1 2 3 |
|
需要注意的是,如果提供的url配置项不是简单路由格式(不以http:或https:开头), 也不是跳转路由格式(fbrward:开头),
那么将会执行Ribbon路由过滤器,将url看作一个 serviceldo下面的配置片断,效果也等同于前面的配置:
1 2 3 4 5 |
|
7.4.4自定义路由规则
如果上面的路由配置无法满足实际需求,可以考虑使用自定义的路由规则。实现方式 较为简单,在配置类中创建一个PattemServiceRouteMapper即可,请见代码
1 2 3 4 5 6 |
|
创建了 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,请见以下配置片断:
1 2 3 4 5 6 |
|
访问/sale路径的请求都会被路由到zuul-sale-service进行处理,但/sale/noRoute除外。
本节将讲解Zuul 一些较为常用的配置。
7.5.1请求头配置
在集群的服务间共享请求头并没有什么问题,但是如果请求会被转发到其他系统,那 么对于敏感的请求头信息,就需要进行处理。
在默认情况下,HTTP请求头的Cookie、 Set-Cookie、 Authorization属性不会传递到"源服务",可以使用sensitiveHeaders属性来配置敏感请求头,
下面的配置对全局生效:
1 2 |
|
以下的配置片断,仅对一个路由生效:
1 2 3 4 5 6 |
|
除了使用sensitiveHeaders属性外,还可以使用ignoredHeaders属性来配置全局忽略的 请求头。使用该配置项后,请求与响应中所配置的头信息均被忽略:
1 2 |
|
7.5.2路由端点
在网关项目中提供了一个/routes服务,可以让我们查看路由映射信息。如果想开启该 服务需要满足以下条件:
一般情况下,Actuator开启了端点的安全认证,即使符合以上两个条件,也无法访问 routes服务。要解决该问题,可以在配置文件中将management.security.enabled属性值设置 为false关闭安全认证。
以7.3节中介绍的项目为例,开启/routes服务后访问http://localhost:8080/routes,浏览 器中将输出以下JSON (以下JSON经过格式化):
1 2 3 4 5 6 7 8 9 |
|
7.5.3 Zuul 与 Hystrix
当我们对网关进行配置让其调用集群的服务时,将会执行Ribbon路由过滤器 (RibbonRoutingFilter);
该过滤器在进行转发时会封装为一个Hystrix命令予以执行。
换言之,它具有容错的功能。如果“源服务”出现问题(例如超时),那么所执行的Hystrix命令将会触发回退,下面将测试Zuul中的回退。
为销售模块(zuul-sale-service)的控制器添加一个超时方法,请见代码
1 2 3 4 5 |
|
在网关项目(zuul-gateway)中建立一个网关处理类,处理回退逻辑,请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
回退处理类需要实现ZuulFallbackProvider接口,实现的getRoute方法返回路由的名称, 该方法将与配置中的路由进行对应,本例配置的路由如下:
1 2 3 4 5 |
|
简单点说就是,zuul-sale-service路由出现问题导致触发回退时,由MyFallbackProvider处 理。MyFallbackProvider 类实现的 fallbackResponse 方法要返回一个 ClientHttpResponse 实例。
本例中返回的ClientHttpResponse,内容为fallback,也就是回退触发时,调用的客户端将 得到fallback字符串。
为了让Spring 容器知道 MyFallbackProvider,在配置类中新建 MyFallbackProvider 的 Bean,如代码:
1 2 3 4 5 6 |
|
启动整个集群,在浏览器中访问以下地址http://localhost:8080/sale/errorTest,浏览器返 回fallback字符串,可见回退被触发。
以上实现的MyFalIbackProvider仅对zuul-sale-service路由有效,如果想对全局有效, 可以使用以下实现:
1 2 3 |
|
7.5.4 在 Zuul 中预加载 Ribbon
调用集群服务时,会使用Ribbon的客户端。默认情况下,客户端相关的Bean会延迟加载,在第一次调用集群服务时,才会初始化这些对象。
在第一次调用时,控制台会有以 下的输出日志(仅截取部分):
1 2 3 |
|
如果想提前加载Ribbon客户端,可以在配置文件中进行以下配置:
1 2 3 4 |
|
以上的配置在Spring容器初始化时,就会创建Ribbon客户端的相关实例。启动网关项 目可以看到以上的输出日志。
至此,Zuul的基本功能已经介绍完毕。掌握了前面章节介绍的内容,基本上就可以使 用Zuul 了。接下来,再进一层,让我们更深入地学习Zuul。
7.6.1过滤器优先级
Spring Cloud为HTTP请求的各个阶段提供了多个过滤器,这些过滤器的执行顺序由它们各自提供的一个int值决定,提供的值越小,优先级越高。
图7-6展示了默认的过滤器, 以及它们的优先级。
如图7-6所示,在routing阶段会优先执行Ribbon路由的过滤器,再执行简单路由过 滤器。
7.6.2自定义过滤器
了解过滤器的执行顺序后,我们编写一个自定义过滤器。新建过滤类,继承ZuulFilter, 实现请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
新建的自定义过滤器将会在routing阶段执行,优先级为1,也就是在routing阶段,该 过滤器最先执行。另外注意shouldFilter方法,过滤最终是否执行由该方法决定,本例返回 true,表示访问任何路由规则都会执行该过滤器。
为了让Spring容器知道过滤器的存在, 需要对该类进行配置,代码清单7-11所示为配置类。
1 2 3 4 5 6 |
|
启动集群,访问网关http://localhost:8080/test/l,会看到控制输出:执行MyFilter过滤 器。
实际上,访问任何一个配置好的路由都会进行输出。
7.6.3动态加载过滤器
相对于集群中的其他节点,网关更需要长期、稳定地提供服务。如果需要增加过滤器, 重启网关代价太大,为了解决该问题,Zuul提供了过滤器的动态加载功能。
可以使用Groovy 来编写过滤器,然后添加到加载目录,让Zuul去动态加载。先为网关项目加入Groovy的 依赖:
1 2 3 4 5 |
|
接下来,在网关项目的应用类中,调用Zuul的API来实现动态加载,请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在启动类中,增加了 zuullnit方法,使用@PostConstruct进行修饰。在该方法中,先读取zuul.filter.root和zuul.filter.refreshlnterval两个属性,分别表示动态过滤器的根目录以及 刷新间隔,刷新间隔以秒为单位。
这两个属性优先读取配置文件的值,如果没有则使用默认值。在配置文件中,可使用下面的配置片断:
1 2 3 4 |
|
调用FilterFileManager的init方法,初始化3个过滤器目录:pre、route和posto为了 测试动态加载,使用Groovy编写一个最简单的过滤器,请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
与前面的过滤器一致,同样继承自ZuulFilter。需要注意的是,本例的过滤器并没有一 开始就放到动态加载的过滤器目录中,读者在测试时,需要先启动网关项目,再将 Dynamic F i 1 ter. groovy放到对应目录中。
完成以上工作后,启动网关项目,访问以下地址http://localhost:8080/test/crazyit,控制 台中并没有输出 DynamicFilter 的信息。
将 DynamicFilter.groovy 复制到 src/main/java/groovy/ filters/route目录,等待几秒后,重新访问以上地址,可以看到网关的控制台输出如下:
1 |
|
7.6.4 禁用过滤器
如果想禁用其中一个过滤器,可以使用以下配置:
1 2 3 4 |
|
以上配置会将SendForwardFilter (处理跳转路由的过滤器)禁用,如果再为url属性使用fbrward:进行配置的话,将不会产生跳转效果。同样,禁同其他过滤器也会导致失去相 应的功能。
7.6.5 请求上下文
HTTP请求的全部信息都封装在_个RequestContext对象中,该对象继承ConcurrentHashMap» 可将RequestContext看作一个Map, RequestContext维护着当前线程的全部请求变量,例如 请求的URI、serviceld、主机等信息。
本小节将以RequestContext为基础,编写一个自定 义的过滤器,使用RestTemplate来调用集群服务。
新建一个过滤器,实现请见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
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加入配置中,请见代码:
1 2 3 4 5 6 7 8 9 10 |
|
在application.yml文件中,建立对应的路由规则,请见以下配置片断:
1 2 3 4 |
|
以上配置片断,设置路由的path为/rest.tpl.sale,当访问该地址时,将会执行前面的 RestTemplateFilter;
启动集群,访问以下地址:http://localhost:8080/rest-tpl-sale/sale-book/1, 浏览器输出返回的字符串SUCCESS,控制台输出如下:
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编写了一个自定 义的过滤器,该过滤器会抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
在ExceptionFilter的shouldFilter方法中,遇到exceptionTest的uri才会执行,目的是不影响本章其他例子的执行。
在run方法中,简单进行控制台打印,再抛出一个ZuulRuntimeException, 该异常实例包装了一个ZuulExceptiono为了查看异常输出的信息,新建一个控制器,主要在控制台中输出这些信息。
请见代码:
1 2 3 4 5 6 7 8 9 10 |
|
MyErrorController 继承了 BasicErrorController, BasicErrorController 是 Spring Boot 中用 于处理错误的控制器基类。在过滤器抛出异常后,SendErrorFilter会跳转到/error路径,然 后就会执行MyErrorController的errorHtml方法返回到错误页面。
在本例中我们不进行处理, 只在方法体中输出此处得到的异常信息。
启动整个集群,访问以下地址http://localhost:8080/ exceptionTest/test,可以看到网关项目的控制台输出如下:
1 2 3 4 5 6 7 8 |
|
根据输出结果可知,过滤器抛出的异常信息可以在错误处理的控制器中获取。
7.6.8动态路由
在前面章节中,所有的路由规则都在application.yml中进行配置,在实际应用中,可 能一个模块就有一份路由配置文件,而且这些配置文件的内容都在不停变化。如果因为部分变化而重启网关,这是无法想象的;
因此,路由规则的动态刷新功能在实际应用中非常 重要。
路由的动态刷新需要以配置文件的更新、配置项的刷新为前提,这部分内容将在Spring Cloud Config章节中讲解,因此动态路由的实现,也在那一章中讲解,本章不进行讲述。
本章以Zuul框架为核心,讲解了Spring Cloud集群中网关的功能。主要演示了在Web 项目、在Spring Cloud中使用Zuul,请求转发、微服务调用等内容较为重要,不仅要学会 如何使用,最好还要知道其实现原理。
本章的7.4节与7.5节介绍了 Zuul的常用配置,掌握这些配置后,基本就可以使用Zuul 了。
7.6节讲解了过滤器的相关内容,学习该节后, 可以清楚地了解过滤器的工作机制,以便在实际环境中实现自己所需要的功能。