Zuul 介绍
什么是Zuul?
Zuul 也是Netflix公司做出的系统,主要是Netflix API流量的数量和多样性有时会导致生产问题迅速出现而没有警告。因此Netflix团队做了这么一个统一的入口——网关,可以将所有的API组织起来。
Zuul是所有从设备和web站点到Netflix流媒体应用程序后端请求的前门。作为一个边缘服务应用程序,Zuul被构建来支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。
Zuul 是一个基于JVM路由和服务端的负载均衡器,Spring Cloud 对Zuul 进行了一系列的封装和时间,并且可以和Eureka、Ribbon、Hystrix、Feign等组件配合使用,对于Spring Cloud 微服务来说不必暴露过多的接口,提升了整个分布式集群的安全性。
为什么构建Zuul?
Zuul使用了一系列不同类型的过滤器,使我们能够快速灵活地将功能应用到服务中。这些过滤器帮助我们执行以下功能:
- 身份验证和安全性: 识别每个资源的身份验证需求,并拒绝不满足它们的请求
- 监控: 在边缘跟踪有意义的数据和统计数据,以便给我们一个准确的生产视图
- 动态路由: 动态路由请求到不同的后端集群
- 压力测试: 逐渐增加集群的流量,以评估性能
- 限流: 为每种请求类型分配容量,并丢弃超过限制的请求
静态响应处理: 直接在边缘构建一些响应,而不是将它们转发到内部集群
Zuul 2.0架构
https://github.com/Netflix/zuul/wiki/How-It-Works-2.0
从较高的角度来看,Zuul 2.0是一台Netty服务器,它运行前置过滤器(入站过滤器),然后使用Netty客户端代理请求,然后在运行后置过滤器(出站过滤器)后返回响应。
过滤器是Zuul业务逻辑的核心所在。它们具有执行大量动作的能力,并且可以在请求-响应生命周期的不同部分运行,如上图所示。入站筛选器在路由到源之前执行,并且可以用于身份验证,路由和修饰请求。
- 端点过滤器可用于返回静态响应,否则内置
ProxyEndpoint过滤器会将请求路由到源。 - 出站筛选器在从源获取响应后执行,可用于度量标准,修饰用户的响应或添加自定义标头。
过滤器也有两种类型:同步和异步。由于我们在事件循环上运行,因此永远不要阻塞过滤器至关重要。如果要阻塞,请在单独的线程池上的异步过滤器中进行阻塞,否则可以使用同步过滤器。
和Zuul 1.*对比
- 前端用Netty Server代替Servlet,目的是支持前端异步。后端用Netty Client代替Http Client,目的是支持后端异步。
- 过滤器换了一下名字,用Inbound Filters代替Pre-routing Filters,用Endpoint Filter代替Routing Filter,用Outbound Filters代替Post-routing Filters。
- 性能提升约20%
本书讲解的
Spring Cloud Hoxton.RELEASE支持的Zuul 版本仍然是1.* 系列,用的1.3.1,因为对于Zuul 的使用和介绍仍然以1.3.1 版本为主。
<dependency><groupId>com.netflix.zuul</groupId><artifactId>zuul-core</artifactId><version>1.3.1</version><scope>compile</scope><exclusions><exclusion><artifactId>groovy-all</artifactId><groupId>org.codehaus.groovy</groupId></exclusion><exclusion><artifactId>mockito-all</artifactId><groupId>org.mockito</groupId></exclusion></exclusions></dependency>
Zuul 简单应用
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
下面通过一个简单项目带领大家入门,我们直接使用Spring Cloud 封装好的包,本节我们要实现当输入Zuul的地址时可以跳转发哦另一个服务上。如下图
1.新建项目
1.1 添加maven 配置
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-zuul</artifactId></dependency></dependencies>
1.2 添加启动类
启动类中需要添加@EnableZuulProxy注解
@EnableZuulProxy@SpringBootApplicationpublic class FwZuulSimpleApplication {public static void main(String[] args) {SpringApplication.run(FwZuulSimpleApplication.class, args);}}
1.3 添加应用配置
我们给zuul添加转发配置,当输入ip:port/simple 会转发到http://localhost:8764
server:port: 8678spring:application:name: fw-gateways-zuul-simplezuul:routes:simple:url: http://localhost:8764 #转发的地址
1.4 启动项目
这里需要fw-cloud-client-eureka服务的支持(8764端口),然后启动当前项目
在Postman 中输入localhost:8678/simple/hello实际上调用的是localhost:8764/hello
2. 运行原理
因为我们开启了@EnableZuulProxy注解,在Spring容器初始化的时候,会将Zuul的相关配置也初始化,Spring Boot提供了ServletRegistrationBean用于注册Servlet,Zuul中有一个类ZuulServlet,在Servlet的service方法中执行各种ZuulFilter。下图是ZuulServlet的生命周期。
zuul把过滤器分为四个阶段,分别是
- pre:主要是在请求路由之前调用,很多验证可以在这里做
- route:在路由请求时候被调用,主要用来转发请求
- post:主要用来处理响应请求
- error:当错误发生时,会经由这个类型的过滤器处理
ZuulServlet的service方法在接收到请求后,会先执行pre阶段的过滤器,再执行routing阶段的过滤器,最后执行post阶段的过滤器,其中routing阶段的过滤器会将请求转发到“源服务”,源服务一般都是第三方服务,也可以是当前集群的其它服务,在实行pre、routing、post阶段发生异常时,会执行error过滤器,整个HTTP请求、响应等数据会被封装到RequestContext对象中。
Zuul路由配置
1. 简单路由
上面的配置方式对运维人员很不友好,因此Spring Cloud 团队实现Spring Cloud Zuul与Spring Cloud Eureka的整合然后进行路由操作了,并且默认的转发规则就是”Zuul网关地址+访问的服务名称+API URL”,所以给定服务名称的时候尽量简短一点,服务名称直接可以从Eureka中获取。
比如:
- 网关地址:
localhost:8678 - 服务名称:
fw-auth - 接口名称:
/hello
通过Zuul网关访问的默认地址就是localhost:8678/fw-auth/hello本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-client/fw-cloud-client-eureka
1.1 应用配置
server:port: 8678spring:application:name: fw-gateways-zuul-simplezuul:routes:simple:url: http://localhost:8764 #转发的地址
上文的简单实用里面,我们说的配置方式合计就是简单路由。url 的配置会匹配http://或https://,如果直接配置localhost:8764将转发不成功。
这种配置方式是一种简单路由,由过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将HttpServletRequest的请求数据转化为HttpClient的请求实例HttpRequest,然后把使用CloseableHttpClient转发。
当然Zuul也支持修改HttpClient的配置属性,
zuul:host:max-total-connections: 300 #设置目标主机的最大连接数,默认200max-per-route-connections: 30 #设置每个主机的初始化连接数,默认20
1.2 应用启动
浏览器或者Postman 输入localhost:8678/simple/hello
2.路由前缀
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
比如我们想给将通过网关的接口统一设置一个url前缀,可以使用如下配置
zuul:prefix: /api
那之前的接口在想请求simple 的数据,就必须带上/api,localhost:8678/simple/hello要变成localhost:8678/api/simple/hello
3. 指定路由
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
3.1 新建项目
3.2 新建启动类
记得开启@EnableZuulProxy注解
@EnableZuulProxy@EnableDiscoveryClient@SpringBootApplicationpublic class FwZuulApplication {public static void main(String[] args) {SpringApplication.run(FwZuulApplication.class, args);}}
3.3 应用配置
server:port: 8679spring:application:name: fw-gateways-zuuleureka:client:service-url:defaultZone: http://localhost:8761/eureka/zuul:routes:fw-cloud-ribbon-server:path: /ribbon/**
当然还可以简写
zuul:routes:fw-cloud-ribbon-server:path: /ribbon/**
也可以写成,path 可以省略
zuul:routes:fw-cloud-ribbon-server: /ribbon/**
3.4 启动项目
浏览器或Postman 输入localhost:8678/ribbon/user/1
4. 路由跳转
在简单路由里已经介绍了跳转到具体第三方地址的配置,下面我们来演示一下本地跳转的功能,Zuul 里面的本地跳转只要通过forward就可以了。
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
4.1 修改应用配置
修改Zuul 部分的配置,添加一个forward的API前缀
zuul:routes:fw-cloud-ribbon-server:path: /ribbon/**url: forward:/local
4.2 添加控制层
fw-cloud-gateways-zuul添加/local控制层
/*** @author xuyisu* @description forward转发* @date 2020/1/11*/@RestControllerpublic class LocalController {@GetMapping("/local/user/{id:\\d+}")public String getId(@PathVariable Long id){return id.toString()+",我是forward转发来的";}}
4.3启动项目

浏览器或者Postman 输入http://localhost:8679/ribbon/user/1
5. Zuul 过滤器
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
Zuul作为网关来说,会做很多转发前的校验,而这些校验我们都可以基于Zuul过滤器来实现,比如Token校验、IP黑名单等。
5.1 新建过滤器
通过模拟用户必须携带token,来拦截用户的请求。前面我们说过Zuul 的生命周期,我们这里模拟在Header里面需要携带token,并且token 的值必须为123456,否则就会执行失败。
/*** @author xuyisu* @description Token Filter* @date 2019/12/12*/public class TokenFilter extends ZuulFilter {@Overridepublic String filterType() {return "pre"; // 在请求被路由之前调用}@Overridepublic int filterOrder() {return 0; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低}@Overridepublic boolean shouldFilter() {return true; // 是否执行该过滤器,此处为true,说明需要过滤}@Overridepublic Object run() {RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();String token = request.getHeader("token");// 获取请求的参数// 如果有token参数并且token值为123456,才进行路由if (StringUtils.isNotBlank(token) && token.equals("123456")) {ctx.setSendZuulResponse(true); //对请求进行路由ctx.setResponseStatusCode(200);ctx.set("code", 1);} else {ctx.setSendZuulResponse(false); //不对其进行路由ctx.setResponseStatusCode(401);HttpServletResponse response = ctx.getResponse();response.setHeader("content-type", "text/html;charset=utf8");ctx.setResponseBody("认证失败");ctx.set("code", 0);}return null;}}
自定义过滤器需要继承ZuulFilter , 并且需要实现下面几个方法:
• shou ldFilter : 是否执行该过滤器, true 为执行, false 为不执行,可以结合配置中心及结合业务逻辑设置
• filterType :过滤器类型,pre 、route 、post 、error 。
• filterOrder :过滤器的执行顺序,数值越小,优先级越高。
• run :执行自己的业务逻辑。设置的ctx.setSendZuulResponse(false); 代表不对其进行路由,ctx.setSendZuulResponse(true);表示对请求进行路由。
5.3 配置过滤器
@Configurationpublic class ZuulConfig {/*** Zuul 过滤器配置,如果不想启动,注释掉即可*/@Beanpublic TokenFilter tokenFilter(){return new TokenFilter();}}
5.4 重启应用
浏览器或Postman 输入localhost:8679/ribbon/user/1
首先未设置token
设置token
6. Zuul 过滤器拦截顺序
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
6.1 新建过滤器
新建一个过滤器,设置执行顺序为5,在TokenFilter之后,是不是这样,待会可以验证一下,这个过滤器是验证header中是否有name这个key,并且key的值要是zuul才能通过,否则失败。
/*** @author xuyisu* @description Zuul Filter* @date 2019/12/28*/@Slf4jpublic class ZuulFilter extends com.netflix.zuul.ZuulFilter {@Overridepublic String filterType() {return "pre"; // 在请求被路由之前调用}@Overridepublic int filterOrder() {return 5; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低}@Overridepublic boolean shouldFilter() {return true; // 是否执行该过滤器,此处为true,说明需要过滤}@Overridepublic Object run() {RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();log.info("我是ZuulFilter");String token = request.getHeader("name");// 获取请求的参数// 如果有name参数并且token值为zuul,才进行路由if (StringUtils.isNotBlank(token) && token.equals("zuul")) {ctx.setSendZuulResponse(true); //对请求进行路由ctx.setResponseStatusCode(200);ctx.set("code", 1);} else {ctx.setSendZuulResponse(false); //不对其进行路由ctx.setResponseStatusCode(401);HttpServletResponse response = ctx.getResponse();response.setHeader("content-type", "text/html;charset=utf8");ctx.setResponseBody("请携带网关必须参数");ctx.set("code", 0);}return null;}}
6.2 添加过滤器配置
@Beanpublic ZuulFilter zuulFilter(){return new ZuulFilter();}
6.3 重启项目
浏览器或Postman输入localhost:8679/ribbon/user/1
首先全部设置正确的header

并且我们看到控制台输出的日志,确实是filterOrder越小越先执行
2020-01-04 18:14:11.379 INFO 10940 --- [nio-8679-exec-2] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenFilter2020-01-04 18:14:11.385 INFO 10940 --- [nio-8679-exec-2] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilter
去掉token 验证

发现虽然验证失败,按理说应该不会执行第二个过滤器,但是执行了,看控制台日志
2020-01-04 18:23:49.714 INFO 10940 --- [nio-8679-exec-5] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenFilter2020-01-04 18:23:49.715 INFO 10940 --- [nio-8679-exec-5] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilter
原因是什么呢?
Zuul中Filter的执行逻辑如下:在ZuuLServlet中的service方法
中执行对应的Filter,比如preRoute()。preRoute()中会通过zuulRunner来执行
首先请求经过ZuulServlet执行
@Overridepublic void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {try {init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);// Marks this request as having passed through the "Zuul engine", as opposed to servlets// explicitly bound in web.xml, for which requests will not have the same data attachedRequestContext context = RequestContext.getCurrentContext();context.setZuulEngineRan();try {preRoute();} catch (ZuulException e) {error(e);postRoute();return;}try {route();} catch (ZuulException e) {error(e);postRoute();return;}try {postRoute();} catch (ZuulException e) {error(e);return;}} catch (Throwable e) {error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));} finally {RequestContext.getCurrentContext().unset();}}
执行pre类型的过滤器
/*** executes "pre" filters** @throws ZuulException*/void preRoute() throws ZuulException {zuulRunner.preRoute();}
调用FilterProcessor的preRoute()
public void preRoute() throws ZuulException {FilterProcessor.getInstance().preRoute();}
然后preRoute()调用runFilters()获取所有过滤器并执行
public void preRoute() throws ZuulException {try {this.runFilters("pre");} catch (ZuulException var2) {throw var2;} catch (Throwable var3) {throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());}}public Object runFilters(String sType) throws Throwable {if (RequestContext.getCurrentContext().debugRouting()) {Debug.addRoutingDebug("Invoking {" + sType + "} type filters");}boolean bResult = false;List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);if (list != null) {for(int i = 0; i < list.size(); ++i) {ZuulFilter zuulFilter = (ZuulFilter)list.get(i);Object result = this.processZuulFilter(zuulFilter);if (result != null && result instanceof Boolean) {bResult |= (Boolean)result;}}}return bResult;}
由此,可以知道为什么第一个报错了,为什么第二个过滤器也执行。
那么如何让第一个停了之后不执行第二个过滤器了呢?请看下一节数据传递
7. 数据传递
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
上节我们说过过滤器的拦截顺序,现在继续分析拦截器如何在第一个没通过,就不会执行第二个。
7.1 分析源码
通过分析Zuul 的这段源码,可以看出,先判断这个过滤器是不是已经禁用了,禁用的话不执行,然后判断shouldFilter(),我们可以通过设置shouldFilter()的true、false 来控制过滤器的执行。
public ZuulFilterResult runFilter() {ZuulFilterResult zr = new ZuulFilterResult();if (!this.isFilterDisabled()) {if (this.shouldFilter()) {Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());try {Object res = this.run();zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);} catch (Throwable var7) {t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");zr = new ZuulFilterResult(ExecutionStatus.FAILED);zr.setException(var7);} finally {t.stopAndLog();}} else {zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);}}return zr;}
7.2 数据传递
因此我们可以在第一个失败的时候,通过RequestContext传递变量,为什么用RequestContext,主要还是因为RequestContext的实现原理是基于ThreadLocal实现的,源码如下
private static RequestContext testContext = null;protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {protected RequestContext initialValue() {try {return (RequestContext)RequestContext.contextClass.newInstance();} catch (Throwable var2) {throw new RuntimeException(var2);}}};
在TokenFilter中设置如下值。
//失败之后通知后续不应该执行了ctx.set("isShould",false);
将ZuulFilter 中的shouldFilter()修改成如下。
@Overridepublic boolean shouldFilter() {RequestContext requestContext=RequestContext.getCurrentContext();Boolean isShould = (Boolean) requestContext.get("isShould");return null==isShould?true:isShould; // 是否执行该过滤器,此处为true,说明需要过滤}
7.3 项目重启
浏览器或Postman 输入localhost:8679/ribbon/user/1
控制台中我可以看到只输出一条日志了
2020-01-03 21:44:02.236 INFO 19344 --- [nio-8679-exec-1] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenF
8.Zuul 禁用过滤器
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
如果想禁用一个Zuul过滤器,我们通过如下方式
配置中将相应的Bean 初始掉
/*** Zuul 过滤器配置,如果不想启动,注释掉即可*/// @Bean// public TokenFilter tokenFilter(){// return new TokenFilter();// }
在配置打开禁用的开关
TokenFilter 就是过滤器的类名zuul:TokenFilter:route:disabled: true
8.1 重启项目
浏览器或Postman 输入localhost:8679/ribbon/user/1,Header 里面什么都没放

可以看到,由于我们把TokenFilter 禁用了,也就是不起作用了,仅仅ZuulFilter打印出了日志,控制台日志如下2020-01-03 21:57:24.112 INFO 18400 --- [nio-8679-exec-1] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilte
9. Zuul 异常处理
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
对于过滤器的各阶段在执行时,异常主要发生在run()方法中,Zuul 为我们提供了异常处理的过滤器,方法执行时抛出的异常会被捕获到并且调用RequestContext.setThrowable 方法设置异常,error 阶段的SendErrorFilter会判断RequestContext中是否存在异常,如果存在会执行SendErrorFilter过滤器。
SendErrorFilter过滤器在执行的时候,会将异常信息写到HttpServletRequest中,再调用RequestDispatcher 的forward方法,默认会跳转到/error页面,我们这里会实现一个/error 的接口,让错误信息可以被我们自定义成JSON输出。
9.1 新建异常过滤器
主要是通过Throwable throwable = ctx.getThrowable();获取异常信息
/*** @author xuyisu* @description 异常处理过滤器* @date 2020/1/3*/@Slf4jpublic class ErrorFilter extends ZuulFilter {@Overridepublic String filterType() {return "error";}@Overridepublic int filterOrder() {return 10;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() {RequestContext ctx=RequestContext.getCurrentContext();Throwable throwable = ctx.getThrowable();ctx.setSendZuulResponse(false); //不对其进行路由ctx.setResponseStatusCode(401);HttpServletResponse response = ctx.getResponse();response.setHeader("content-type", "text/html;charset=utf8");ctx.setResponseBody("认证失败"+throwable.getCause().getMessage());ctx.set("code", 500);log.error("异常信息,{}",throwable.getCause().getMessage());return null;}}
9.2 添加配置
@Beanpublic ErrorFilter errorFilter(){return new ErrorFilter();}
9.3 在TokenFilter添加一个异常
int i=10/0;
9.4 新建异常处理处理类
FwResult是我统一封装的返回类,代码就不贴了,可以到源码里面看看。
/*** @author xuyisu* @description 异常处理类* @date 2020/1/3*/@RestControllerpublic class ErrorHandlerController implements ErrorController {@Autowiredprivate ErrorAttributes errorAttributes;@Overridepublic String getErrorPath() {return "/error";}@RequestMapping("/error")public FwResult error(HttpServletRequest request){Map<String, Object> errorAttributes = this.errorAttributes.getErrorAttributes(new ServletWebRequest(request), true);String message = (String) errorAttributes.get("message");String trace = (String) errorAttributes.get("trace");if(StrUtil.isNotBlank(trace)){message=message+",trace is "+trace;}return FwResult.failed(message);}}
9.5 重启项目
浏览器或Postman 输入localhost:8679/ribbon/user/1
返回的异常信息正好使我们自定义的。
10. 重试机制
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
之前我们在介绍Feign,也讲到的重试机制,现在我们来说一下Zuul的重试机制,希望Zuul 在转发服务的时候,如果失败可以重试几次,可能第一次是网络抖动,或者转发到相同服务名是的其它地址上面。
spring-retry是spring提供的一个基于spring的重试框架,我们直接使用这个框架。
10.1 maven 配置添加
<!--重试包--><dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency>
10.2 修改应用配置
zuul:retryable: true
这个开关默认是关闭的,开启后的配置和Ribbon 配置一样,如下
#重试ribbon:#配置首台服务器重试1次MaxAutoRetries: 1#配置其他服务器重试两次MaxAutoRetriesNextServer: 2#链接超时时间ConnectTimeout: 500#请求处理时间ReadTimeout: 500#每个操作都开启重试机制OkToRetryOnAllOperations: true
这里可以直接使用Ribbon 的配置,是因为zuul 里面已经集成了Ribbon的包
10.3 在Ribbon Server添加随机延时
超过500秒就会重试
int millis = new Random().nextInt(3000);System.out.println("client线程休眠时间:"+millis);Thread.sleep(millis);
10.4 重启项目
浏览器或Postman 输入localhost:8679/ribbon/user/1
可以看到控制台重试的日志
这里和Feign 章节说的重试是一样的,可以回顾一下
11.Zuul FallBack回调
本节代码地址
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple
GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-ribbon/fw-cloud-ribbon-server
虽然上一节我们配置了重试机制,但是重试次数结束了没成功任然会失败,比如如下错误,因为Spring Cloud Zuul 里面已经集成了Hystrix,我们可以自己定义回退机制。
Caused by: java.net.SocketTimeoutException: Read timed out
11.1 新建FallBack 类
实现回退需要实现FallbackProvider接口
/*** @author xuyisu* @description fallback* @date 2020/1/4*/@Slf4j@Componentpublic class ZuulFallBack implements FallbackProvider {@Overridepublic String getRoute() {return "*";}@Overridepublic ClientHttpResponse fallbackResponse(String route, Throwable cause) {return new ClientHttpResponse() {@Overridepublic HttpStatus getStatusCode() throws IOException {return HttpStatus.OK;}@Overridepublic int getRawStatusCode() throws IOException {return this.getStatusCode().value();}@Overridepublic String getStatusText() throws IOException {return this.getStatusCode().getReasonPhrase();}@Overridepublic void close() {}@Overridepublic InputStream getBody() throws IOException {RequestContext ctx=RequestContext.getCurrentContext();Throwable throwable = ctx.getThrowable();if(null!=throwable){log.error("Zuul发生错误,{}",throwable.getCause().getMessage());FwResult<String> byteMsg = FwResult.failed(throwable.getCause().getMessage(), "网络或服务异常");return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());}FwResult<String> byteMsg = FwResult.failedMsg("网络或服务异常");return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());}@Overridepublic HttpHeaders getHeaders() {HttpHeaders headers=new HttpHeaders();MediaType mediaType=new MediaType("application", "json", StandardCharsets.UTF_8);headers.setContentType(mediaType);return headers;}};}}
- getRoute 方法中返回*表示对所有服务进行回退操作,如果只想对某个服务进行回退,
那么就返回需要回退的服务名称,这个名称是注册到Eureka中的名称 - ClientHttpResponse 构造回退的内容
- getStatusCode 返回响应的状态码
- getStatusText 返回响应状态码对应的文本
- getBody 返回回退的内容
- getHeaders 返回响应的请求头信息
11.2 设置hystrix 超时时间
假设设置1秒#回退超时时间hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 1000
11.3 重启服务
浏览器或Postman 输入地址localhost:8679/ribbon/user/1,如果想快速看到,可以将fw-cloud-ribbon-server关掉
