本文主要是介绍SpringCloud 服务网关Zuul Hoxton版本,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Spring Cloud Zuul简介:Spring Cloud Zuul是Spring Cloud Netflix子项目的核心组件之一,作为微服务API网关,具有动态路由、过滤、压力测试、监控、弹性伸缩和安全等功能,并且能够与Eureka、Ribbon、Hystrix等组件配合使用。
本文主要对Spring Cloud Zuul的基本使用进行简单总结,其中SpringBoot使用的2.2.2.RELEASE
版本,SpringCloud使用的Hoxton.SR1
版本。这里将沿用SpringCloud 服务注册与发现Eureka Hoxton版本的eureka-server
作为注册中心,eureka-client
作为服务生产者,还有ribbon
、hystrix
和openfeign
这3个服务也都是前几篇文章中创建的。
一、创建服务网关
通过Maven新建一个名为spring-cloud-netflix-zuul
的项目。
1.引入依赖
SpringBoot和SpringCloud依赖这里就不列出来了,还需引入以下依赖,其中spring-retry
在配置重试的时候用,这里提前引入了。
<!-- Spring Cloud Zuul 起步依赖 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- Spring Cloud Eureka Client 起步依赖 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Actuator 起步依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- SpringRetry 重试框架依赖 -->
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId>
</dependency>
2.主启动类
package com.rtxtitanv;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.ZuulApplication* @description 主启动类* @date 2020/3/2 12:06*/
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {public static void main(String[] args) {SpringApplication.run(ZuulApplication.class, args);}
}
@EnableZuulProxy
:启用Zuul的API网关功能。
3.编写配置文件
在application.yml
中进行如下配置:
server:port: ${PORT:1500}spring:application:name: zuuleureka:client:# 服务注册,是否将服务注册到Eureka注册中心,true:注册,false:不注册register-with-eureka: true# 服务发现,是否从Eureka注册中心获取注册信息,true:获取,false:不获取fetch-registry: true# 配置Eureka注册中心即Eureka服务端的地址,集群地址以,隔开service-url:defaultZone: http://rtxtitanv:rtxtitanv@eureka-server-01:8001/eureka/,http://rtxtitanv:rtxtitanv@eureka-server-02:8002/eureka/,http://rtxtitanv:rtxtitanv@eureka-server-03:8003/eureka/instance:# 将ip地址注册到Eureka注册中心prefer-ip-address: true# 该服务实例在注册中心的唯一实例ID,${spring.cloud.client.ip-address}获取该服务实例ipinstance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}# 该服务实例向注册中心发送心跳间隔,单位秒,默认30秒lease-renewal-interval-in-seconds: 20# Eureka注册中心在删除此实例之前收到最后一次心跳后的等待时间,单位秒,默认90秒lease-expiration-duration-in-seconds: 60management:endpoints:web:exposure:# 暴露指定端点,routes为路由端点include: 'routes'
4.查看路由信息
这时候一个基本的服务网关就搭建好了,由于通过SpringBoot Actuator可以查看Zuul的路由信息,所以引入了spring-boot-starter-actuator
依赖并配置了management.endpoints.web.exposure.include='routes'
暴露路由端点。IDEA启动eureka-server
集群,eureka-client
集群和ribbon
,hystrix
,openfeign
和zuul
,访问注册中心,下图为服务注册信息:
访问http://localhost:1500/actuator/routes查看路由信息:
访问http://localhost:1500/actuator/routes/details查看详细路由信息:
5.实现路由功能
由于Zuul里面集成了Ribbon,所以可以进行Ribbon的配置和通过Ribbon实现负载均衡。不断访问http://localhost:1500/eureka-client/home,根据下面动图中的测试过程和结果,说明成功路由到了eureka-client
服务的/home
接口并实现了负载均衡。
二、路由配置
1.默认路由规则
在没有显式配置路由规则时依然能查看到路由信息并实现路由功能,这是因为Zuul结合Eureka使用时,它为Eureka中的每一个服务都自动创建一个默认路由规则,这些默认规则的path
会使用serviceId
配置的服务名作为请求前缀,相当于以下配置:
zuul:# 路由配置routes:# serviceName为路由名serviceName:path: /serviceName/**serviceId: serviceName
2.忽略默认路由规则配置
默认情况Zuul会为所有Eureka服务自动创建映射关系来进行路由,这会使不希望对外开放的服务也可以被外部访问到,这时候需要在自动创建默认路由规则时忽略那些不希望对外开放的服务。可以通过zuul.ignored-services
设置一个服务名匹配表达式来指定需要忽略的服务,Zuul会在自动创建服务路由时会根据该表达式判断,如果服务名匹配则跳过不创建默认路由规则。下面在application.yml
中新增以下配置,指定忽略默认路由规则的服务:
zuul:# 指定忽略默认路由规则的服务,*表示所有服务都不创建默认路由规则ignored-services: eureka-server,eureka-client,ribbon,hystrix,openfeign
访问http://localhost:1500/actuator/routes查看路由信息,下图为路由信息,为空说明指定服务都成功忽略默认路由规则。
3.传统路由配置
传统路由配置就是不依赖于服务发现机制的方式,直接在配置文件中指定每个路由表达式与服务实例的映射关系来实现API网关对外请求的路由。
(1)单实例配置
通过zuul.routes.<route>.path
与zuul.routes.<route>.url
的方式进行配置,其中<route>
为路由名称,不能有相同的路由名称,每个<route>
对应了一条路由规则,即匹配客户端请求的路径表达式与具体实例地址或服务名的映射。下面在application.yml
中进行如下配置:
zuul:# 传统路由配置之单实例配置routes:# 配置路由规则,route-eureka-client为路由名,每一个路由名对应一条路由规则# 路由规则为将与/eureka-client-api/**匹配的请求路径转发到http://localhost:9001/route-eureka-client:# 指定匹配客户端请求的路径表达式path: /eureka-client-api/**# 指定匹配客户端请求的路径表达式映射的具体实例地址url: http://localhost:9001/
不断访问http://localhost:1500/eureka-client-api/home,根据下面动图中的测试过程和结果,说明成功路由到了eureka-client
服务的9001端口节点的/home
接口。
(2)多实例配置
通过zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
的方式进行配置并通过ribbon.eureka.enabled=false
停用Eureka,<client>.ribbon.listOfServers
手动配置服务列表,这里的<client>
与serviceId
对应。下面在application.yml
中进行如下配置:
zuul:# 传统路由配置之多实例配置routes:# 配置路由规则,route-eureka-client为路由名,每一个路由名对应一条路由规则# 路由规则为将与/eureka-client-api/**匹配的请求路径转发到eureka-clientroute-eureka-client:# 指定匹配客户端请求的路径表达式path: /eureka-client-api/**# 指定匹配客户端请求的路径表达式映射的服务名serviceId: eureka-clientribbon:eureka:# 是否使用Eureka,true:使用,false:禁用,默认为true,禁用后需手动配置服务列表enabled: false
eureka-client:ribbon:# 禁用Eureka后手动配置服务列表listOfServers: localhost:9001,localhost:9002,localhost:9003
不断访问http://localhost:1500/eureka-client-api/home,根据下面动图中的测试过程和结果,说明成功路由到了eureka-client
服务的/home
接口并实现了负载均衡。
4.服务路由配置
通过zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
的方式进行配置,由于Zuul结合使用了Eureka,所以不需要手动指定服务地址列表。下面在application.yml
中进行如下配置:
zuul:# 服务路由配置routes:# 配置路由规则,route-eureka-client为路由名,每一个路由名对应一条路由规则# 路由规则为将与/eureka-client-api/**匹配的请求路径转发到eureka-clientroute-eureka-client:# 指定匹配客户端请求的路径表达式path: /eureka-client-api/**# 指定匹配客户端请求的路径表达式映射的服务名serviceId: eureka-client
以上配置还有一种更简单的写法,通过zuul.routes.<serviceId>=<path>
来实现,其中<serviceId>
为服务名称,<path>
为匹配客户端请求的表达式。下面在application.yml
中进行如下配置:
zuul:routes:# 服务路由配置的简写方式,eureka-client为服务名,/eureka-client-api/**为匹配客户端请求的路径表达式eureka-client: /eureka-client-api/**
不断访问http://localhost:1500/eureka-client-api/home,根据下面动图中的测试过程和结果,说明成功路由到了eureka-client
服务的/home
接口并实现了负载均衡。
5.自定义路由映射规则
如果想自定义路由映射规则,可以使用regexmapper
在serviceId
和路由之间提供约定。它使用名为groups的正则表达式从serviceId
中提取变量并将它们注入到路由模式中。下面是一个自定义路由映射规则的配置:
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}
根据以上配置,如果serviceId
为appname-v1
,会被映射到路由/v1/appname/**
。任何正则表达式都被接受,但所有命名组都必须存在于servicePattern
和routePattern中
。如果servicePattern
与serviceId
不匹配,则使用默认规则。比如serviceId
为appname
,检测不到版本,会被映射到/appname/**
,使用默认规则,仅适用于已发现的服务。具体的测试这里就省略了。
6.路径匹配
在Zuul中,匹配客户端请求的路径表达式<path>
有三种通配符,?
匹配任意单个字符,*
匹配任意数量的字符,**
匹配任意数量的字符并支持多及目录。
当一个url路径被多个不同路由规则的表达式匹配,匹配结果取决于路由规则的保存顺序,即路由规则配置的顺序,优先级按路由规则配置的顺序由高到低。由于需要路由规则的保存顺序,需要使用YAML文件,因为properties文件会丢失路由规则的保存顺序。例如以下配置,先判断url是否与/eureka-client-api/a/**
匹配,匹配就选择/eureka-client-api/a/**
路由,如果不匹配就判断是否与/eureka-client-api/**
匹配,匹配就选择/eureka-client-api/**
路由,不匹配就往下依次判断直到最后一个路由规则。
zuul:routes:hello-service-ext:path: /eureka-client-api/a/**serviceId: eureka-client-ahello-service:path: /eureka-client-api/**serviceId: eureka-client
7.忽略路由表达式
通过zuul.ignored-patterns
可以指定匹配忽略客户端请求的路径表达式,与这些表达式匹配的请求路径不会被API网关进行路由转发,该配置对所有路由有效。下面在application.yml
中进行如下配置:
zuul:# 指定匹配忽略客户端请求的路径表达式,对所有路由有效ignored-patterns: /**/home/**
访问http://localhost:1500/eureka-client-api/home,结果见下图,访问失败说明忽略成功。
8.路由前缀
通过zuul.prefix
可以为全局的路由规则增加前缀,指定了路由前缀之后在访问API网关时需要加上前缀。下面在application.yml
中进行如下配置:
zuul:# 指定路由前缀,为全局的路由规则增加前缀信息prefix: /api
访问http://localhost:1500/api/eureka-client-api/home,根据下图中的测试结果,说明成功路由到了eureka-client
服务的/home
接口。
前缀在代理转发时会默认从路径中移除,可以通过zuul.stripPrefix=false
指定全局的路由规则在代理转发时不移除前缀,也可以通过zuul.routes.<route>.strip-prefix=false
来指定该路由规则在代理转发时不移除前缀。下面在application.yml
中进行如下配置:
zuul:# 指定全局的路由规则在转发时是否移除前缀,true:移除,false:不移除strip-prefix: false
访问http://localhost:1500/api/eureka-client-api/home,结果见下图,访问失败说明转发时没有移除前缀。
9.本地跳转
通过zuul.routes.<route>.url
中配置forward形式的路径可以实现本地跳转。下面在application.yml
中新增以下配置:
zuul:routes:# 以forward形式的服务跳转配置# 路由规则为将与/zuul-api/**匹配的请求路径转发到API网关中以/local为前缀的请求route-local:# 指定匹配客户端请求的路径表达式path: /zuul-api/**# 指定本地跳转的路径前缀url: forward:/local
在本地提供/local/home
接口:
package com.rtxtitanv.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.controller.ZuulController* @description ZuulApiController 提供本地接口用于本地跳转测试* @date 2020/3/2 18:57*/
@RequestMapping("/local")
@RestController
public class ZuulController {@GetMapping("/home")public String home() {return "跳转到本地";}
}
访问http://localhost:1500/zuul-api/home,根据下图中的测试结果,说明成功跳转到本地/local/home
接口。
10.Header相关配置
(1)过滤敏感Header配置
Zuul在请求路由时默认会过滤HTTP请求头中的一些敏感信息,包括Cookie、Set-Cookie、Authorization三个属性,防止它们被传递到下游服务。Cookie在Zuul网关中默认不传递,当使用了一些安全框架时Cookie无法传递会导致认证授权失败。通过zuul.sensitive-headers=
全局设置为空可以覆盖默认值不过滤敏感头,或者通过zuul.routes.<route>.custom-sensitive-headers=true
开启自定义敏感头,zuul.routes.<route>.sensitive-headers=
设置为空覆盖全局配置指定路由规则不过滤敏感头。下面在application.yml
中新增以下配置:
zuul:routes:route-eureka-client:# 指定该路由规则过滤的敏感头,设置为空表示不过滤sensitive-headers:# 指定是否开启自定义敏感头,true:开启,false:关闭custom-sensitive-headers: true
(2)忽略Header配置
通过zuul.ignored-headers
可以设置需要忽略的请求头,这些请求头在下游服务之间通信时会被过滤,不会被传递。下面是忽略请求头的配置:
zuul:# 指定忽略的请求头,这些请求头在下游服务之间通信时会被过滤,不会被传递ignored-headers: header1,header2
如果引入了SpringSecurity,SpringSecurity会自动添加以上配置,如果下游服务还需要使用SpringSecurity的Header时,可以配置zuul.ignore-security-headers=false
。下面是不忽略SpringSecurity请求头的配置:
zuul:# 指定是否忽略SpringSecurity的请求头,true:忽略,false:不忽略ignore-security-headers: false
sensitive-headers
只会过滤客户端附带的请求头,在客户端请求中附带这些请求头,API网关在不会传递给下游服务。ignored-headers
过滤的是服务之间通信附带的请求头,如果客户端请求中附带这些请求头,API网关会传递给下游服务,下游服务再转发就会被过滤。
(3)重定向配置
通过zuul.add-host-header=true
可以解决重定向跳转后,Zuul在路由时host信息不正确的问题。下面是重定向时添加host请求头的配置:
zuul:# 指定重定向时是否添加host请求头信息,true:添加,false:不添加add-host-header: true
11.Zuul饥饿模式
Zuul内部使用Ribbo实现负载均衡,默认情况在调用时会延迟加载Ribbon客户端,可以通过zuul.ribbon.eager-load.enabled=true
开启Zuul饥饿模式,启动时就立即加载。下面在application.yml
中新增以下配置:
zuul:ribbon:eager-load:# 指定是否启用Zuul饥饿模式,true:启用,false:不启用enabled: true
12.重试开关
通过zuul.retryable
可以配置全局路由是否开启重试,设置true表示全局路由开启重试,false表示全局路由关闭重试。通过zuul.routes.<route>.retryable
则可以配置指定路由是否开启重试。
13.普通嵌入式Zuul
如果使用@EnableZuulServer
而不是@EnableZuulProxy
,也可以运行不带代理的Zuul服务或选择性地切换部分代理平台。添加到ZuulFilter
类型的应用程序的bean将自动装载,与@EnableZuulProxy
一样,但不会自动添加任何代理过滤器。这种情况下仍然通过zuul.routes.*
来指定Zuul服务的路由,但没有服务发现和代理,因此serviceId
和url
设置将被忽略,例如以下配置,是将/api/**
中的所有路径映射到Zuul过滤器链。
zuul:routes:api: /api/**
三、Zuul中配置Ribbon和Hystrix
1.超时重试
需先引入spring-retry
依赖,由于该依赖之前已经引入了,这里就不列出来了,然后在application.yml
中新增以下配置:
zuul:# 指定全局路由是否开启重试,true:开启,false:关闭retryable: true# ribbon全局配置
ribbon:# 处理请求的超时时间,单位ms,默认1000ReadTimeout: 3000# 连接建立的超时时间,单位ms,默认1000ConnectTimeout: 3000# 切换实例的最大重试次数,不包括首次调用,默认0次MaxAutoRetriesNextServer: 1# 对当前实例的最大重试次数,不包括首次调用,默认1次MaxAutoRetries: 1# 是否对所有操作请求都进行重试,true:是,false:否,只针对get请求进行重试# 设置为true时,如果是put或post等写操作,如果服务器接口不能保证幂等性,会产生不好的结果,所以OkToRetryOnAllOperations设置为true需慎用# 默认情况下,get请求无论是连接异常还是读取异常,都会进行重试,非get请求,只有连接异常时,才会进行重试OkToRetryOnAllOperations: false# 对指定Http响应码进行重试retryableStatusCodes: 404,500,502hystrix:command:# hystrix command参数全局配置default:execution:timeout:# 是否启用hystrix超时,true:启用,false:不启用enabled: trueisolation:thread:# hystrix超时时间,需大于ribbon的超时时间,单位ms# hystrix超时时间需大于(MaxAutoRetries+1)(MaxAutoRetriesNextServer+1)(ConnectTimeout+ReadTimeout)timeoutInMilliseconds: 30000
不断访问http://localhost:1500/eureka-client-api/home,在测试过程中先停掉一个eureka-client
节点,然后再停掉一个eureka-client
节点,根据下面动图中的测试过程和结果,说明超时重试成功。
2.服务降级
Zuul在进行路由转发时发生了错误而转发失败时,想要进行服务降级需要实现FallbackProvider接口并重写该接口的两个方法,下面是服务降级类:
package com.rtxtitanv.fallback;import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.fallback.ZuulFallback* @description 服务降级类* @date 2020/3/3 22:42*/
@Component
public class ZuulFallback implements FallbackProvider {/*** 指定需要降级的服务,如果所有服务都支持降级,返回"*"或null** @return 需要降级的服务名*/@Overridepublic String getRoute() {return "*";}/*** 自定义服务降级返回的内容** @param route route* @param cause cause* @return ClientHttpResponse*/@Overridepublic ClientHttpResponse fallbackResponse(String route, Throwable cause) {return new ClientHttpResponse() {@Overridepublic HttpStatus getStatusCode() throws IOException {return HttpStatus.OK;}@Overridepublic int getRawStatusCode() throws IOException {return HttpStatus.OK.value();}@Overridepublic String getStatusText() throws IOException {return HttpStatus.OK.getReasonPhrase();}@Overridepublic void close() {}@Overridepublic InputStream getBody() throws IOException {return new ByteArrayInputStream("error fallback!".getBytes());}@Overridepublic HttpHeaders getHeaders() {HttpHeaders httpHeaders = new HttpHeaders();httpHeaders.setContentType(MediaType.APPLICATION_JSON);return httpHeaders;}};}
}
根据下面动图中的测试过程和结果,说明路由转发失败时降级成功。
3.Hystrix监控
在使用HystrixDashboard监控Zuul之前,先查看SpringCloud 断路器监控HystrixDashboard与Turbine Hoxton版本,启动hystrix-dashboard
,然后在Zuul服务网关中暴露hystrix.stream
端点,然后查看Zuul服务网关的监控信息,下图为Zuul服务网关的监控信息:
ThreadPools信息一直处于Loading状态,是因为Zuul默认采用的信号量隔离策略,需要设置为线程池策略,在Zuul里面通过zuul.ribbon-isolation-strategy=THREAD
设置线程池隔离策略,通过zuul.thread-pool.use-separate-thread-pools=true
指定每个路由使用独立的线程,通过zuul.thread-pool.thread-pool-key-prefix
指定HystrixThreadPoolKey的前缀。下面在application.yml
中新增以下配置:
zuul:# 指定隔离策略,THREAD:线程池隔离策略ribbon-isolation-strategy: THREADthread-pool:# 指定是否让每个路由使用独立的线程池,true:是,false:否use-separate-thread-pools: true# 指定HystrixThreadPoolKey的前缀thread-pool-key-prefix: zuulkey-
查看Zuul服务网关的监控信息,下图为Zuul服务网关的监控信息:
四、过滤器
1.过滤器主要特征
Zuul过滤器包括下列四个主要特征:
- Type:路由流转期间过滤器被应用时定义的常见阶段。
- Execution Order:应用于过滤器类型中限定在多个过滤器的执行顺序。
- Criteria:过滤器在执行前所需要的条件。
- Action:执行条件满足时将要执行的操作。
这四个特征对应IZuulFilter接口和ZuulFilter抽象类中定义的下面列出的四个抽象方法,其中ZuulFilter实现了IZuulFilter。
String filterType();
int filterOrder();
boolean shouldFilter();
Object run() throws ZuulException;
下面是对这四个方法主要功能的总结:
- filterType:设置过滤器的类型。
- filterOrder:设置过滤器的执行顺序,数值越小优先级越高。
- shouldFilter:判断该过滤器是否需要执行。
- run:过滤器执行逻辑。
2.过滤器类型
对应典型的请求生命周期,有几种标准的过滤器类型:
- PRE:请求路由到目标服务之前执行,例如权限认证,选择源服务,日志调试。
- ROUTING:请求路由到目标服务时执行,这是使用Apache HttpClient或Netflix Ribbon构建和发送原始HTTP请求的地方。
- POST:请求路由到目标服务后执行,比如给目标服务HTTP响应添加头信息,收集统计数据和指标,响应从源服务流式传输到客户端。
- ERROR:请求在其他阶段发生错误时执行。
3.Zuul请求生命周期
Zuul默认定义的四种不同类型的过滤器,对应了一个HTTP请求从发送再到API网关,直到返回结果的整个生命周期。下图描述一个HTTP请求到达API网关之后在各个不同类型的过滤器之间流转的详细过程。
4.核心过滤器
Spring Cloud Zuul中为HTTP请求生命周期的各个阶段默认地实现了一批核心过滤器,它们会在API网关服务启动的时候被自动地加载和启用,下表对Zuul中自带的核心过滤器进行了总结。
过滤器 | 类型 | 顺序 | 功能 |
---|---|---|---|
DebugFilter | pre | 1 | 标记调试标志 |
FormBodyWrapperFilter | pre | -1 | 包装请求体 |
PreDecorationFilter | pre | 5 | 处理请求上下文供后续使用 |
Servlet30WrapperFilter | pre | -2 | 包装HttpServletRequest请求 |
ServletDetectionFilter | pre | -3 | 标记处理Servlet的类型 |
RibbonRoutingFilter | route | 10 | serviceId请求转发 |
SendForwardFilter | route | 500 | forward请求转发 |
SimpleHostRoutingFilter | route | 100 | url请求转发 |
LocationRewriteFilter | post | 900 | 重定向时负责将头重写为Zuul的url |
SendResponseFilter | post | 1000 | 处理正常的请求响应 |
SendErrorFilter | error | 0 | 处理有错误的请求响应 |
5.自定义过滤器
(1)pre类型过滤器
PreFilter:
package com.rtxtitanv.filter;import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.filter.PreFilter* @description pre类型的自定义过滤器* @date 2020/3/2 12:07*/
@Component
public class PreFilter extends ZuulFilter {private static Logger logger = LoggerFactory.getLogger(PreFilter.class);/*** 过滤器类型,有pre、routing、post、error四种** @return 代表过滤器类型的字符串*/@Overridepublic String filterType() {return "pre";}/*** 过滤器执行顺序,数值越小优先级越高** @return 代表过滤器执行顺序的int值*/@Overridepublic int filterOrder() {return 0;}/*** 是否进行过滤** @return true:过滤,false:不过滤*/@Overridepublic boolean shouldFilter() {return true;}/*** 自定义的过滤器逻辑,当shouldFilter()返回true时会执行** @return Object* @throws ZuulException*/@Overridepublic Object run() throws ZuulException {// 获取请求上下文RequestContext requestContext = RequestContext.getCurrentContext();// 获取请求对象HttpServletRequest request = requestContext.getRequest();logger.info("method:{},url:{}", request.getMethod(), request.getRequestURL().toString());// 获取请求参数token的值String token = request.getParameter("token");// token不为null,空串或空白字符串时进行路由转发,否则不进行路由转发if (StringUtils.isNotBlank(token)) {logger.info("存在token");// 对请求进行路由requestContext.setSendZuulResponse(true);// 设置响应状态码requestContext.setResponseStatusCode(200);requestContext.set("tokenIsExist", true);return null;} else {logger.warn("token不存在");// 不对请求进行路由requestContext.setSendZuulResponse(false);// 设置响应状态码requestContext.setResponseStatusCode(401);// 获取响应对象HttpServletResponse response = requestContext.getResponse();response.setContentType("application/json; charset=utf8");response.setCharacterEncoding("utf8");PrintWriter writer = null;try {writer = response.getWriter();writer.write("token不存在");} catch (IOException ioException) {logger.error("message: " + ioException.getMessage());} finally {if (writer != null) {writer.flush();writer.close();}}requestContext.set("tokenIsExist", false);return null;}}
}
重启Zuul服务网关之后,自定义过滤器装载到Spring容器中生效,访问http://localhost:1500/eureka-client-api/home,下图是访问结果:
访问http://localhost:1500/eureka-client-api/home?token=abc,下图是访问结果:
根据以上测试结果,说明请求成功在路由到目标服务之前进行了过滤。
(2)post类型过滤器
PostFilter:
package com.rtxtitanv.filter;import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.filter.PostFilter* @description post类型的自定义过滤器* @date 2020/3/2 12:08*/
@Component
public class PostFilter extends ZuulFilter {private static Logger logger = LoggerFactory.getLogger(PostFilter.class);private static final String USERNAME = "admin";private static final String PASSWORD = "admin";/*** 过滤器类型,有pre、routing、post、error四种** @return 代表过滤器类型的字符串*/@Overridepublic String filterType() {return "post";}/*** 过滤器执行顺序,数值越小优先级越高** @return 代表过滤器执行顺序的int值*/@Overridepublic int filterOrder() {return 0;}/*** 是否进行过滤** @return true:过滤,false:不过滤*/@Overridepublic boolean shouldFilter() {RequestContext requestContext = RequestContext.getCurrentContext();// 判断token是否存在,token存在才进行过滤return (boolean)requestContext.get("tokenIsExist");}/*** 自定义的过滤器逻辑,当shouldFilter()返回true时会执行** @return Object* @throws ZuulException*/@Overridepublic Object run() throws ZuulException {// 获取请求上下文RequestContext requestContext = RequestContext.getCurrentContext();// 获取请求对象HttpServletRequest request = requestContext.getRequest();logger.info("method:{},url:{}", request.getMethod(), request.getRequestURL().toString());// 获取请求参数username和password的值String username = request.getParameter("username");String password = request.getParameter("password");// username和passsword正确则进行路由转发,否则不进行路由转发if (StringUtils.isNotBlank(username) && USERNAME.equals(username) && StringUtils.isNotBlank(password)&& PASSWORD.equals(password)) {logger.info("用户名密码正确");// 对请求进行路由requestContext.setSendZuulResponse(true);// 设置响应状态码requestContext.setResponseStatusCode(200);requestContext.set("isVerify", true);return null;} else {logger.warn("用户名或密码不正确");// 不对请求进行路由requestContext.setSendZuulResponse(false);// 设置响应状态码requestContext.setResponseStatusCode(401);// 获取响应对象HttpServletResponse response = requestContext.getResponse();response.setContentType("application/json; charset=utf8");response.setCharacterEncoding("utf8");OutputStream writer = null;try {// 避免 getWriter() has already been called for this response 问题writer = response.getOutputStream();writer.write("用户名或密码不正确".getBytes());} catch (IOException ioException) {logger.error("message: " + ioException.getMessage());} finally {if (writer != null) {try {writer.flush();writer.close();} catch (IOException ioException) {ioException.printStackTrace();}}}requestContext.set("isVerify", false);return null;}}
}
访问http://localhost:1500/eureka-client-api/home?username=admin&&password=admin,下图是访问结果:
访问http://localhost:1500/eureka-client-api/home?token=abc&&username=admin&&password=adnim,下图是访问结果:
访问http://localhost:1500/eureka-client-api/home?token=abc&&username=admin&&password=admin,下图是访问结果:
根据以上测试结果,说明请求成功在路由到目标服务之后进行了过滤。
(3)error类型过滤器
ErrorFilter:
package com.rtxtitanv.filter;import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.filter.ErrorFilter* @description error类型的自定义过滤器* @date 2020/3/3 23:36*/
@Component
public class ErrorFilter extends ZuulFilter {private static Logger logger = LoggerFactory.getLogger(ErrorFilter.class);private static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";/*** 过滤器类型,有pre、routing、post、error四种** @return 代表过滤器类型的字符串*/@Overridepublic String filterType() {return "error";}/*** 过滤器执行顺序,数值越小优先级越高** @return 代表过滤器执行顺序的int值*/@Overridepublic int filterOrder() {return 0;}/*** 是否进行过滤** @return true:过滤,false:不过滤*/@Overridepublic boolean shouldFilter() {RequestContext requestContext = RequestContext.getCurrentContext();return requestContext.getThrowable() != null && !requestContext.getBoolean(SEND_ERROR_FILTER_RAN, false);}/*** 自定义的过滤器逻辑,当shouldFilter()返回true时会执行** @return Object* @throws ZuulException*/@Overridepublic Object run() throws ZuulException {// 获取请求上下文RequestContext requestContext = RequestContext.getCurrentContext();// 获取ZuulExceptionZuulException zuulException = this.findZuulException(requestContext.getThrowable());logger.error("系统异常拦截开始", zuulException);// 获取响应对象HttpServletResponse response = requestContext.getResponse();response.setContentType("application/json; charset=utf8");response.setCharacterEncoding("utf8");response.setStatus(zuulException.nStatusCode);PrintWriter writer = null;try {writer = response.getWriter();writer.print("code:" + zuulException.nStatusCode + ",message:\"" + zuulException.getMessage() + "\"");} catch (IOException ioException) {logger.error("message: " + ioException.getMessage());} finally {if (writer != null) {writer.flush();writer.close();}}return null;}/*** 获取ZuulException** @param throwable throwable* @return ZuulException*/private ZuulException findZuulException(Throwable throwable) {if (throwable.getCause() instanceof ZuulRuntimeException) {return (ZuulException)throwable.getCause().getCause();}if (throwable.getCause() instanceof ZuulException) {return (ZuulException)throwable.getCause();}if (throwable instanceof ZuulException) {return (ZuulException)throwable;}return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);}
}
使用自定义error过滤器需要禁用自带的error过滤器SendErrorFilter,在application.yml
中进行如下配置:
zuul:SendErrorFilter:error:# 禁用SendErrorFilter过滤器,true:禁用,false:不禁用disable: true
为了测试errorFilter,新建一个pre类型过滤器ThrowExceptionFilter来抛出一个异常,执行顺序设置为-1小于PreFilter的0,先于PreFilter执行方便测试。ThrowExceptionFilter代码如下:
package com.rtxtitanv.filter;import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;/*** @author rtxtitanv* @version 1.0.0* @name com.rtxtitanv.filter.ThrowExceptionFilter* @description pre类型的自定义过滤器,抛出异常,测试error过滤器* @date 2020/3/3 23:37*/
@Component
public class ThrowExceptionFilter extends ZuulFilter {private static Logger logger = LoggerFactory.getLogger(ThrowExceptionFilter.class);/*** 过滤器类型,有pre、routing、post、error四种** @return 代表过滤器类型的字符串*/@Overridepublic String filterType() {return "pre";}/*** 过滤器执行顺序,数值越小优先级越高** @return 代表过滤器执行顺序的int值*/@Overridepublic int filterOrder() {return -1;}/*** 是否进行过滤** @return true:过滤,false:不过滤*/@Overridepublic boolean shouldFilter() {return true;}/*** 自定义的过滤器逻辑,当shouldFilter()返回true时会执行** @return Object* @throws ZuulException*/@Overridepublic Object run() throws ZuulException {logger.info("抛出一个RuntimeException进行测试");this.throwException();return null;}/*** 抛出一个RuntimeException,用于测试error过滤器*/private void throwException() {throw new RuntimeException("error");}
}
访问http://localhost:1500/eureka-client-api/home,下图是访问结果:
访问http://localhost:1500/eureka-client-api/home?token=abc,下图是访问结果:
根据以上测试结果,说明请求成功在路由过程中发生错误时进行了过滤。
6.禁用过滤器
上面已经禁用过了Zuul自带的过滤器SendErrorFilter,通过zuul.<SimpleClassName>.<filterType>.disable=true
可以禁用过滤器,其中<SimpleClassName>
为过滤器类名,不是全限定类名,<filterType>
为过滤器类型。下面在application.yml
中新增以下配置:
zuul:PreFilter:pre:# 禁用PreFilter过滤器,true:禁用,false:不禁用disable: truePostFilter:post:# 禁用PostFilter过滤器,true:禁用,false:不禁用disable: trueErrorFilter:error:# 禁用ErrorFilter过滤器,true:禁用,false:不禁用disable: trueThrowExceptionFilter:pre:# 禁用ThrowExceptionFilter过滤器,true:禁用,false:不禁用disable: true
重启Zuul服务网关后测试发现PreFilter,PostFilter,ErrorFilter和ThrowExceptionFilter禁用成功,具体的测试过程这里就省略了。
代码示例
- Github:https://github.com/RtxTitanV/springcloud-learning/tree/master/springcloud-hoxton-learning/spring-cloud-netflix-zuul
- Gitee:https://gitee.com/RtxTitanV/springcloud-learning/tree/master/springcloud-hoxton-learning/spring-cloud-netflix-zuul
这篇关于SpringCloud 服务网关Zuul Hoxton版本的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!