Zuul整合Spring
我们搭建一个简单的服务来演示下如何在Spring中继承Zuul使用。首先看下简单的架构图:
建立一个Zuul-Spring项目,包含下面4个子项目:
- zuul-eureka-server:Eureka服务器,应用端口为8761
- zuul-book-service:书本模块,服务提供者,提供REST /book/{bookId}接口,用于查找图书,最后返回Book的JSON字符串,应用端口为9000
- zuul-sale-service:销售模块,服务调用者,对外发布销售服务,/sale-book/{bookId},在zuul-book-service来查找Book,应用端口为9100
- zuul-gateway:网关服务,提供路由转发功能,将请求转发到销售服务上
zuul-eureka-server
其pom依赖为:
1 | <dependencies> |
配置文件为:
1 | server: |
启动类为:
1 |
|
zuul-book-service
服务提供方,其pom文件为:
1 | <dependencies> |
RestController为:
1 |
|
Book类:
1 | public class Book { |
启动类:
1 |
|
zuul-sale-service
pom:
1 | <dependencies> |
配置文件:
1 | server: |
RestController:
1 |
|
Service:
1 |
|
Book类即为zuul-book-service中的Book类,这里不再赘述
启动类:
1 |
|
zuul-gateway
pom:
1 | <dependencies> |
配置文件:
1 | spring: |
启动类:
1 |
|
按照如下顺序启动各个服务后:
- zuul-eureka-server
- zuul-book-service
- zuul-sale-service
- zuul-gateway
调用接口http://localhost:8080/sale/sale-book/1,就能看到调用结果了
接着就能在zuul-sale-service的日志能找到如下日志:
路由配置
简单路由
Spring Cloud在Zuul的routing阶段实现了几个过滤器,这些过滤器决定如何进行路由工作。其中,最基本的就是SimpleHostRoutingFilter,该过滤器运行后,会将HTTP请求全部转发到“源服务”。
以下为简单路由的配置,同时使用了path和url:
1 | zuul: |
以上的配置访问http://localhost:8080/routeTest/163,将会跳转到163网站。
为了配置渐变,可以省略path,默认情况下使用routeId作为path:
1 | zuul: |
访问http://localhost:8080/route163,同样会路由到163网站。实际上,要出发简单路由,配置的url的值需要以http或者https字符串开头。以下的配置不能出发简单路由:
1 | zuul: |
简单路由的过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将HttPServletRequest的相关数据(HTTP方法、参数、请求头等)转换为HttpClient的请求实例(HttpRequest),再使用CloseableHttpClient进行转发。
再此过程中,为了保证转发的性能,使用了HttpClient的连接池功能。设计连接池,就需要对其进行配置。在使用简单路由时,可以配置一下两项,修改HttpClient连接池的属性:
- zuul.host.maxTotalConnections:目标主机的最大连接数,默认值为200.配置该项,相当于调用了PoolingHttpClientConnectionManager的setMaxTotal方法
- zuul.host.maxPerRouteConnections:每个主机的初始连接数,默认值为20.配置该项,相当于调用了PoolingHttpClientConnectionManager的setDefaultMaxPerRoute方法。
跳转路由
当外部方位网关的A地址时,会跳转到B地址,处理跳转路由的过滤器为SendForwardFilter。
在zuul-gateway中添加如下配置:
1 | zuul: |
在zuul-gateway中添加如下REST Controller:
1 |
|
它做的就是将/test/xx 等接口,转移到/source/hello接口上。
输入地址http://localhost:8080/test/qq访问,就能看到以下结果:
跳转路由实现较为简单,实际上调用了RequestDispatcher的forward方法进行跳转。
Ribbon路由
当网关作为Eureka客户端注册到Eureka服务器时,可以配置通过serviceID将请求转发到集群的服务中。使用以下配置,可以执行Ribbon路由过滤器:
1 | zuul: |
与简单路由类似,serviceID也可以被省略。当省略时,将会使用routeId作为serviceId,下面的配置,等同于上面的配置:
1 | zuul: |
需要注意的是,如果提供的url配置项不是简单路由格式(不以http或者https开头),也不是跳转路由格式(forward开头),那么将会执行Ribbon路由过滤器,将url看做一个serviceId。下面的配置,也等同于前面的配置:
1 | zuul: |
自定义路由规则
如果上面的路由配置无法满足实际需求,可以考虑使用自定义的路由规则。实现方式较为简单,在配置类中创建一个PatternServiceRouteMapper即可。
1 |
|
创建了PatternServiceRouteMapper实例,构造器的第一个参数为serviceId的正则表达式,第二个参数为路由的path。访问module/**的请求,将会被路由到zuul-module-service的微服务。
更进一步,以上的路由规则,如果想让一个或多个服务不被路由,可以使用zuul.ignoredServices属性。如果向排除zuul-sale-service、zuul-book-service这两个模块,可以配置zuul.ignoredServices: zuul-sale-service,zuul-book-service
忽略路由
除了上面提到的zuul.ignoredServices配置可以忽略路由外,还可以使用zuul.ignoredPatterns来设置不进行路由的URL。
1 | zuul: |
访问/sale路径的请求都会被路由到zuul-sale-service进行处理,但/sale/noRoute除外。
Zuul的其他配置
请求头配置
在集群的服务间共享请求头并没有什么问题,但是如果请求会被转发到其他系统,那么对于敏感的请求头信息,就需要进行处理。在默认情况下,HTTP请求头的Cookie、Set-Cookie、Authorization属性不会传递到“源服务”,可以使用sensitiveHeader属性来配置敏感请求头,下面的配置对全局生效:
1 | zuul: |
以下的配置片段,仅对一个路由生效:
1 | zuul: |
除了使用sensitiveHeader属性外,还可以使用ignoredHeaders属性来配置全局忽略的请求头。使用该配置项后,请求与响应中所配置的头信息均被忽略。
1 | zuul: |
路由端点
在网关项目中提供了一个/routes服务,可以让我们查看路由映射信息。如果想开启该服务,需要满足一下条件:
- 网关项目中引入了Spring Boot Actuator
- 项目中使用了@EnableZuulProxy注解
一般情况下Actuator开启了端点的安全认证,即使符合以上两个条件,也无法访问routes服务。要解决该问题,可以在配置文件中将management.security.enabled属性设置为false关闭安全认证。
1 |
|
Zuul和Hystrix
当我们对网关进行配置让其调用集群的服务时,将会执行Ribbon路由过滤器。该过滤器在进行转发时会封装为一个Hystrix命令予以执行。换言之,它具有容错的功能。如果“源服务”出现问题,那么所执行的Hystrix命令将会触发回退。下面将会测试Zuul中的回退。
为zuul-sale-service的控制器添加一个超市方法
1 |
|
在zuul-gateway中建立一个网关处理类,处理回退逻辑
1 | public class MyFallbackProvider implements ZuulFallbackProvider { |
回退处理类需要实现ZuulFallbackProvider接口,实现的getRoute方法返回路由的名称,该方法将与配置中的路由进行对应,本例配置的路由如下:
1 | zuul: |
简单说就是,zuul-sale-service路由出现问题导致触发回退时,由MyFallbackProvider处理。MyFallbackProvider类实现的fallbackResponse方法要返回一个CLientHttpResponse实例。
为了让Spring容器知道MyFallbackProvider,在配置类中新建MyFallbackProvider的Bean。
1 |
|
启动服务后,访问如下地址:http://localhost:8080/sale/errorTest,就能看到浏览器返回“fallback”字符,可见回退被触发。
进阶
过滤器优先级
Spring Cloud为HTTP请求的各个阶层提供了多个过滤器,这些过滤器的执行顺序由它们各自提供的一个int值决定,提供的值越小,优先级越高。
自定义过滤器
新建过滤器类,继承ZuulFilter。
1 | public class MyFilter extends ZuulFilter { |
为了让Spring容器知道过滤器的存在,需要对该类进行配置
1 |
|
重启网关服务,随便访问一个地址,就能看到过滤器被执行了
动态加载过滤器
相对于集群的其他节点,网关更需要长期、稳定地提供服务。如果需要增加过滤器,重启网关的代价太大,为了解决该问题,Zuul提供了过滤器的动态加载功能。可以使用Groovy来编写过滤器,然后添加到加载目录,让Zuul去动态加载。
先增加Groovy的依赖:
1 | <dependency> |
在zuul-gateway的启动类中,调用Zuul的API来实现动态加载
1 |
|
新建一个Groovy脚本
1 | class DynamicFilter extends ZuulFilter { |
重启zuul-gateway,往groovy/filters/route目录下丢这个groovy文件,最多5秒之后,zuul-gateway就能检测到这个拦截器,并自动加载。
禁用过滤器
可以通过如下配置,禁用特定的过滤器:
1 | zuul: |
请求上下文
HTTP请求的信息全部封装在一个RequestContext对象中,该对象继承ConcurrentHashMap。可将RequestContext看做一个Map,RequestContext维护着当前线程的全部请求变量,例如请求的URI、serviceId、主机等信息。
借下面这个例子,说明RequestContext的使用方式
1 | public class RestTemplateFilter extends ZuulFilter { |
RestTemplateFilter的主要功能时使用RestTemplate来调用集群服务。过滤器中的shouldFilter方法从RequestContext中获取HttpServletRequest,再得到请求的uri,如果uri含有rest-tpl-sale字符串,才执行本过滤器。
RestTemplateFilter的执行方法中,从RequestContext中获取了serviceId以及请求的uri,再组合成一个url给RestTemplate执行,执行返回的结果被设置到RequestContext中。
需要注意的是,最后调用了RequestContext的sendZuulResponse方法来设置响应标识。
调用了该方法后,Spring Cloud自带的Ribbon路由过滤器、简单过滤器将不会执行。
在FilterConfig中新增以下代码片段:
1 |
|
然后在zuul-gateway中的配置文件中,增加以下配置:
1 | restTestRoute: |
以上配置片段,设置路由的path为/rest-tpl-sale,当访问该地址时,将会执行前面的RestTemplateFilter。
访问以下地址:http://localhost:8080/rest-tpl-sale/sale-book/1就能得到如下结果
控制台上也能看到
@EnableZuulServer注解
@EnableZuulServer注解,也可以开启Zuul功能,但是使用它之后,SimpleHostRoutingFilter、RibbonRoutingFilter等过滤器将不会开启。
error过滤器
各阶段的过滤器执行时,抛出的异常会被捕获,然后调用RequestContext的SetThrowable方法设置异常。error阶段的SendErrorFilter过滤器会判断RequestContext中是否存在异常(getThrowable是否为null),如果存在,才会执行SendErrorFilter过滤器。
SendErrorFilter过滤器在执行时,会将异常信息设置到HttpServletRequest中,再调用RequestDispatcher的forward方法,默认跳转到/error页面。