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 - 图1
    过滤器是Zuul业务逻辑的核心所在。它们具有执行大量动作的能力,并且可以在请求-响应生命周期的不同部分运行,如上图所示。

  • 入站筛选器在路由到源之前执行,并且可以用于身份验证,路由和修饰请求。

  • 端点过滤器可用于返回静态响应,否则内置ProxyEndpoint过滤器会将请求路由到源。
  • 出站筛选器在从源获取响应后执行,可用于度量标准,修饰用户的响应或添加自定义标头。

过滤器也有两种类型:同步和异步。由于我们在事件循环上运行,因此永远不要阻塞过滤器至关重要。如果要阻塞,请在单独的线程池上的异步过滤器中进行阻塞,否则可以使用同步过滤器。

和Zuul 1.*对比

  1. 前端用Netty Server代替Servlet,目的是支持前端异步。后端用Netty Client代替Http Client,目的是支持后端异步。
  2. 过滤器换了一下名字,用Inbound Filters代替Pre-routing Filters,用Endpoint Filter代替Routing Filter,用Outbound Filters代替Post-routing Filters。
  3. 性能提升约20%

    本书讲解的Spring Cloud Hoxton.RELEASE支持的Zuul 版本仍然是1.* 系列,用的1.3.1,因为对于Zuul 的使用和介绍仍然以1.3.1 版本为主。

  1. <dependency>
  2. <groupId>com.netflix.zuul</groupId>
  3. <artifactId>zuul-core</artifactId>
  4. <version>1.3.1</version>
  5. <scope>compile</scope>
  6. <exclusions>
  7. <exclusion>
  8. <artifactId>groovy-all</artifactId>
  9. <groupId>org.codehaus.groovy</groupId>
  10. </exclusion>
  11. <exclusion>
  12. <artifactId>mockito-all</artifactId>
  13. <groupId>org.mockito</groupId>
  14. </exclusion>
  15. </exclusions>
  16. </dependency>

Zuul 简单应用

本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple


下面通过一个简单项目带领大家入门,我们直接使用Spring Cloud 封装好的包,本节我们要实现当输入Zuul的地址时可以跳转发哦另一个服务上。如下图
网关Zuul - 图2

1.新建项目

网关Zuul - 图3

1.1 添加maven 配置

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  9. </dependency>
  10. </dependencies>

1.2 添加启动类

启动类中需要添加@EnableZuulProxy注解

  1. @EnableZuulProxy
  2. @SpringBootApplication
  3. public class FwZuulSimpleApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(FwZuulSimpleApplication.class, args);
  6. }
  7. }

1.3 添加应用配置

我们给zuul添加转发配置,当输入ip:port/simple 会转发到http://localhost:8764

  1. server:
  2. port: 8678
  3. spring:
  4. application:
  5. name: fw-gateways-zuul-simple
  6. zuul:
  7. routes:
  8. simple:
  9. url: http://localhost:8764 #转发的地址

1.4 启动项目

这里需要fw-cloud-client-eureka服务的支持(8764端口),然后启动当前项目
在Postman 中输入localhost:8678/simple/hello实际上调用的是localhost:8764/hello
网关Zuul - 图4

2. 运行原理

因为我们开启了@EnableZuulProxy注解,在Spring容器初始化的时候,会将Zuul的相关配置也初始化,Spring Boot提供了ServletRegistrationBean用于注册ServletZuul中有一个类ZuulServlet,在Servletservice方法中执行各种ZuulFilter。下图是ZuulServlet的生命周期。
网关Zuul - 图5
zuul把过滤器分为四个阶段,分别是

  • pre:主要是在请求路由之前调用,很多验证可以在这里做
  • route:在路由请求时候被调用,主要用来转发请求
  • post:主要用来处理响应请求
  • error:当错误发生时,会经由这个类型的过滤器处理

ZuulServletservice方法在接收到请求后,会先执行pre阶段的过滤器,再执行routing阶段的过滤器,最后执行post阶段的过滤器,其中routing阶段的过滤器会将请求转发到“源服务”,源服务一般都是第三方服务,也可以是当前集群的其它服务,在实行preroutingpost阶段发生异常时,会执行error过滤器,整个HTTP请求、响应等数据会被封装到RequestContext对象中。

Zuul路由配置

1. 简单路由


上面的配置方式对运维人员很不友好,因此Spring Cloud 团队实现Spring Cloud ZuulSpring Cloud Eureka的整合然后进行路由操作了,并且默认的转发规则就是”Zuul网关地址+访问的服务名称+API URL”,所以给定服务名称的时候尽量简短一点,服务名称直接可以从Eureka中获取。
比如:


1.1 应用配置

  1. server:
  2. port: 8678
  3. spring:
  4. application:
  5. name: fw-gateways-zuul-simple
  6. zuul:
  7. routes:
  8. simple:
  9. url: http://localhost:8764 #转发的地址

上文的简单实用里面,我们说的配置方式合计就是简单路由。url 的配置会匹配http://https://,如果直接配置localhost:8764将转发不成功。
这种配置方式是一种简单路由,由过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将HttpServletRequest的请求数据转化为HttpClient的请求实例HttpRequest,然后把使用CloseableHttpClient转发。
当然Zuul也支持修改HttpClient的配置属性,

  1. zuul:
  2. host:
  3. max-total-connections: 300 #设置目标主机的最大连接数,默认200
  4. max-per-route-connections: 30 #设置每个主机的初始化连接数,默认20

1.2 应用启动

浏览器或者Postman 输入localhost:8678/simple/hello
网关Zuul - 图6

2.路由前缀

本节代码地址

GitHub: https://github.com/xuyisu/fw-sping-cloud/tree/master/fw-cloud-gateways/fw-cloud-gateways-zuul-simple


比如我们想给将通过网关的接口统一设置一个url前缀,可以使用如下配置

  1. zuul:
  2. prefix: /api

那之前的接口在想请求simple 的数据,就必须带上/api,localhost:8678/simple/hello要变成localhost:8678/api/simple/hello
网关Zuul - 图7

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 新建项目

网关Zuul - 图8

3.2 新建启动类

记得开启@EnableZuulProxy注解

  1. @EnableZuulProxy
  2. @EnableDiscoveryClient
  3. @SpringBootApplication
  4. public class FwZuulApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(FwZuulApplication.class, args);
  7. }
  8. }

3.3 应用配置

  1. server:
  2. port: 8679
  3. spring:
  4. application:
  5. name: fw-gateways-zuul
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://localhost:8761/eureka/
  10. zuul:
  11. routes:
  12. fw-cloud-ribbon-server:
  13. path: /ribbon/**

当然还可以简写

  1. zuul:
  2. routes:
  3. fw-cloud-ribbon-server:
  4. path: /ribbon/**

也可以写成,path 可以省略

  1. zuul:
  2. routes:
  3. fw-cloud-ribbon-server: /ribbon/**

3.4 启动项目

浏览器或Postman 输入localhost:8678/ribbon/user/1
网关Zuul - 图9

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前缀

  1. zuul:
  2. routes:
  3. fw-cloud-ribbon-server:
  4. path: /ribbon/**
  5. url: forward:/local

4.2 添加控制层

fw-cloud-gateways-zuul添加/local控制层

  1. /**
  2. * @author xuyisu
  3. * @description forward转发
  4. * @date 2020/1/11
  5. */
  6. @RestController
  7. public class LocalController {
  8. @GetMapping("/local/user/{id:\\d+}")
  9. public String getId(@PathVariable Long id){
  10. return id.toString()+",我是forward转发来的";
  11. }
  12. }

4.3启动项目

网关Zuul - 图10
浏览器或者Postman 输入http://localhost:8679/ribbon/user/1
网关Zuul - 图11

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,否则就会执行失败。

  1. /**
  2. * @author xuyisu
  3. * @description Token Filter
  4. * @date 2019/12/12
  5. */
  6. public class TokenFilter extends ZuulFilter {
  7. @Override
  8. public String filterType() {
  9. return "pre"; // 在请求被路由之前调用
  10. }
  11. @Override
  12. public int filterOrder() {
  13. return 0; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
  14. }
  15. @Override
  16. public boolean shouldFilter() {
  17. return true; // 是否执行该过滤器,此处为true,说明需要过滤
  18. }
  19. @Override
  20. public Object run() {
  21. RequestContext ctx = RequestContext.getCurrentContext();
  22. HttpServletRequest request = ctx.getRequest();
  23. String token = request.getHeader("token");// 获取请求的参数
  24. // 如果有token参数并且token值为123456,才进行路由
  25. if (StringUtils.isNotBlank(token) && token.equals("123456")) {
  26. ctx.setSendZuulResponse(true); //对请求进行路由
  27. ctx.setResponseStatusCode(200);
  28. ctx.set("code", 1);
  29. } else {
  30. ctx.setSendZuulResponse(false); //不对其进行路由
  31. ctx.setResponseStatusCode(401);
  32. HttpServletResponse response = ctx.getResponse();
  33. response.setHeader("content-type", "text/html;charset=utf8");
  34. ctx.setResponseBody("认证失败");
  35. ctx.set("code", 0);
  36. }
  37. return null;
  38. }
  39. }

自定义过滤器需要继承ZuulFilter , 并且需要实现下面几个方法:
• shou ldFilter : 是否执行该过滤器, true 为执行, false 为不执行,可以结合配置中心及结合业务逻辑设置
• filterType :过滤器类型,pre 、route 、post 、error 。
• filterOrder :过滤器的执行顺序,数值越小,优先级越高。
• run :执行自己的业务逻辑。设置的ctx.setSendZuulResponse(false); 代表不对其进行路由,ctx.setSendZuulResponse(true);表示对请求进行路由。

5.3 配置过滤器

  1. @Configuration
  2. public class ZuulConfig {
  3. /**
  4. * Zuul 过滤器配置,如果不想启动,注释掉即可
  5. */
  6. @Bean
  7. public TokenFilter tokenFilter(){
  8. return new TokenFilter();
  9. }
  10. }

5.4 重启应用

浏览器或Postman 输入localhost:8679/ribbon/user/1
首先未设置token
网关Zuul - 图12
设置token
网关Zuul - 图13

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才能通过,否则失败。

  1. /**
  2. * @author xuyisu
  3. * @description Zuul Filter
  4. * @date 2019/12/28
  5. */
  6. @Slf4j
  7. public class ZuulFilter extends com.netflix.zuul.ZuulFilter {
  8. @Override
  9. public String filterType() {
  10. return "pre"; // 在请求被路由之前调用
  11. }
  12. @Override
  13. public int filterOrder() {
  14. return 5; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
  15. }
  16. @Override
  17. public boolean shouldFilter() {
  18. return true; // 是否执行该过滤器,此处为true,说明需要过滤
  19. }
  20. @Override
  21. public Object run() {
  22. RequestContext ctx = RequestContext.getCurrentContext();
  23. HttpServletRequest request = ctx.getRequest();
  24. log.info("我是ZuulFilter");
  25. String token = request.getHeader("name");// 获取请求的参数
  26. // 如果有name参数并且token值为zuul,才进行路由
  27. if (StringUtils.isNotBlank(token) && token.equals("zuul")) {
  28. ctx.setSendZuulResponse(true); //对请求进行路由
  29. ctx.setResponseStatusCode(200);
  30. ctx.set("code", 1);
  31. } else {
  32. ctx.setSendZuulResponse(false); //不对其进行路由
  33. ctx.setResponseStatusCode(401);
  34. HttpServletResponse response = ctx.getResponse();
  35. response.setHeader("content-type", "text/html;charset=utf8");
  36. ctx.setResponseBody("请携带网关必须参数");
  37. ctx.set("code", 0);
  38. }
  39. return null;
  40. }
  41. }

6.2 添加过滤器配置

  1. @Bean
  2. public ZuulFilter zuulFilter(){
  3. return new ZuulFilter();
  4. }

6.3 重启项目

浏览器或Postman输入localhost:8679/ribbon/user/1

首先全部设置正确的header

网关Zuul - 图14
并且我们看到控制台输出的日志,确实是filterOrder越小越先执行

  1. 2020-01-04 18:14:11.379 INFO 10940 --- [nio-8679-exec-2] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenFilter
  2. 2020-01-04 18:14:11.385 INFO 10940 --- [nio-8679-exec-2] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilter

去掉token 验证

网关Zuul - 图15
发现虽然验证失败,按理说应该不会执行第二个过滤器,但是执行了,看控制台日志

  1. 2020-01-04 18:23:49.714 INFO 10940 --- [nio-8679-exec-5] c.yisu.gateways.zuul.filter.TokenFilter : 我是TokenFilter
  2. 2020-01-04 18:23:49.715 INFO 10940 --- [nio-8679-exec-5] c.yisu.gateways.zuul.filter.ZuulFilter : 我是ZuulFilter

原因是什么呢?

ZuulFilter的执行逻辑如下:在ZuuLServlet中的service方法
中执行对应的Filter,比如preRoute()preRoute()中会通过zuulRunner来执行
首先请求经过ZuulServlet执行

  1. @Override
  2. public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
  3. try {
  4. init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
  5. // Marks this request as having passed through the "Zuul engine", as opposed to servlets
  6. // explicitly bound in web.xml, for which requests will not have the same data attached
  7. RequestContext context = RequestContext.getCurrentContext();
  8. context.setZuulEngineRan();
  9. try {
  10. preRoute();
  11. } catch (ZuulException e) {
  12. error(e);
  13. postRoute();
  14. return;
  15. }
  16. try {
  17. route();
  18. } catch (ZuulException e) {
  19. error(e);
  20. postRoute();
  21. return;
  22. }
  23. try {
  24. postRoute();
  25. } catch (ZuulException e) {
  26. error(e);
  27. return;
  28. }
  29. } catch (Throwable e) {
  30. error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
  31. } finally {
  32. RequestContext.getCurrentContext().unset();
  33. }
  34. }

执行pre类型的过滤器

  1. /**
  2. * executes "pre" filters
  3. *
  4. * @throws ZuulException
  5. */
  6. void preRoute() throws ZuulException {
  7. zuulRunner.preRoute();
  8. }

调用FilterProcessorpreRoute()

  1. public void preRoute() throws ZuulException {
  2. FilterProcessor.getInstance().preRoute();
  3. }

然后preRoute()调用runFilters()获取所有过滤器并执行

  1. public void preRoute() throws ZuulException {
  2. try {
  3. this.runFilters("pre");
  4. } catch (ZuulException var2) {
  5. throw var2;
  6. } catch (Throwable var3) {
  7. throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());
  8. }
  9. }
  10. public Object runFilters(String sType) throws Throwable {
  11. if (RequestContext.getCurrentContext().debugRouting()) {
  12. Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
  13. }
  14. boolean bResult = false;
  15. List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
  16. if (list != null) {
  17. for(int i = 0; i < list.size(); ++i) {
  18. ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
  19. Object result = this.processZuulFilter(zuulFilter);
  20. if (result != null && result instanceof Boolean) {
  21. bResult |= (Boolean)result;
  22. }
  23. }
  24. }
  25. return bResult;
  26. }

由此,可以知道为什么第一个报错了,为什么第二个过滤器也执行。

那么如何让第一个停了之后不执行第二个过滤器了呢?请看下一节数据传递

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 来控制过滤器的执行。

  1. public ZuulFilterResult runFilter() {
  2. ZuulFilterResult zr = new ZuulFilterResult();
  3. if (!this.isFilterDisabled()) {
  4. if (this.shouldFilter()) {
  5. Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
  6. try {
  7. Object res = this.run();
  8. zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
  9. } catch (Throwable var7) {
  10. t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
  11. zr = new ZuulFilterResult(ExecutionStatus.FAILED);
  12. zr.setException(var7);
  13. } finally {
  14. t.stopAndLog();
  15. }
  16. } else {
  17. zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
  18. }
  19. }
  20. return zr;
  21. }

7.2 数据传递

因此我们可以在第一个失败的时候,通过RequestContext传递变量,为什么用RequestContext,主要还是因为RequestContext的实现原理是基于ThreadLocal实现的,源码如下

  1. private static RequestContext testContext = null;
  2. protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
  3. protected RequestContext initialValue() {
  4. try {
  5. return (RequestContext)RequestContext.contextClass.newInstance();
  6. } catch (Throwable var2) {
  7. throw new RuntimeException(var2);
  8. }
  9. }
  10. };

在TokenFilter中设置如下值。

  1. //失败之后通知后续不应该执行了
  2. ctx.set("isShould",false);

将ZuulFilter 中的shouldFilter()修改成如下。

  1. @Override
  2. public boolean shouldFilter() {
  3. RequestContext requestContext=RequestContext.getCurrentContext();
  4. Boolean isShould = (Boolean) requestContext.get("isShould");
  5. return null==isShould?true:isShould; // 是否执行该过滤器,此处为true,说明需要过滤
  6. }

7.3 项目重启

浏览器或Postman 输入localhost:8679/ribbon/user/1
网关Zuul - 图16
控制台中我可以看到只输出一条日志了

  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过滤器,我们通过如下方式

  1. 配置中将相应的Bean 初始掉

    1. /**
    2. * Zuul 过滤器配置,如果不想启动,注释掉即可
    3. */
    4. // @Bean
    5. // public TokenFilter tokenFilter(){
    6. // return new TokenFilter();
    7. // }
  2. 在配置打开禁用的开关
    TokenFilter 就是过滤器的类名

    1. zuul:
    2. TokenFilter:
    3. route:
    4. disabled: true

    8.1 重启项目

    浏览器或Postman 输入localhost:8679/ribbon/user/1,Header 里面什么都没放
    网关Zuul - 图17
    可以看到,由于我们把TokenFilter 禁用了,也就是不起作用了,仅仅ZuulFilter打印出了日志,控制台日志如下

    1. 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();获取异常信息

  1. /**
  2. * @author xuyisu
  3. * @description 异常处理过滤器
  4. * @date 2020/1/3
  5. */
  6. @Slf4j
  7. public class ErrorFilter extends ZuulFilter {
  8. @Override
  9. public String filterType() {
  10. return "error";
  11. }
  12. @Override
  13. public int filterOrder() {
  14. return 10;
  15. }
  16. @Override
  17. public boolean shouldFilter() {
  18. return true;
  19. }
  20. @Override
  21. public Object run() {
  22. RequestContext ctx=RequestContext.getCurrentContext();
  23. Throwable throwable = ctx.getThrowable();
  24. ctx.setSendZuulResponse(false); //不对其进行路由
  25. ctx.setResponseStatusCode(401);
  26. HttpServletResponse response = ctx.getResponse();
  27. response.setHeader("content-type", "text/html;charset=utf8");
  28. ctx.setResponseBody("认证失败"+throwable.getCause().getMessage());
  29. ctx.set("code", 500);
  30. log.error("异常信息,{}",throwable.getCause().getMessage());
  31. return null;
  32. }
  33. }

9.2 添加配置

  1. @Bean
  2. public ErrorFilter errorFilter(){
  3. return new ErrorFilter();
  4. }

9.3 在TokenFilter添加一个异常

  1. int i=10/0;

9.4 新建异常处理处理类

FwResult是我统一封装的返回类,代码就不贴了,可以到源码里面看看。

  1. /**
  2. * @author xuyisu
  3. * @description 异常处理类
  4. * @date 2020/1/3
  5. */
  6. @RestController
  7. public class ErrorHandlerController implements ErrorController {
  8. @Autowired
  9. private ErrorAttributes errorAttributes;
  10. @Override
  11. public String getErrorPath() {
  12. return "/error";
  13. }
  14. @RequestMapping("/error")
  15. public FwResult error(HttpServletRequest request){
  16. Map<String, Object> errorAttributes = this.errorAttributes.getErrorAttributes(new ServletWebRequest(request), true);
  17. String message = (String) errorAttributes.get("message");
  18. String trace = (String) errorAttributes.get("trace");
  19. if(StrUtil.isNotBlank(trace)){
  20. message=message+",trace is "+trace;
  21. }
  22. return FwResult.failed(message);
  23. }
  24. }

9.5 重启项目

浏览器或Postman 输入localhost:8679/ribbon/user/1
网关Zuul - 图18
返回的异常信息正好使我们自定义的。

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 配置添加

  1. <!--重试包-->
  2. <dependency>
  3. <groupId>org.springframework.retry</groupId>
  4. <artifactId>spring-retry</artifactId>
  5. </dependency>

10.2 修改应用配置

  1. zuul:
  2. retryable: true

这个开关默认是关闭的,开启后的配置和Ribbon 配置一样,如下

  1. #重试
  2. ribbon:
  3. #配置首台服务器重试1次
  4. MaxAutoRetries: 1
  5. #配置其他服务器重试两次
  6. MaxAutoRetriesNextServer: 2
  7. #链接超时时间
  8. ConnectTimeout: 500
  9. #请求处理时间
  10. ReadTimeout: 500
  11. #每个操作都开启重试机制
  12. OkToRetryOnAllOperations: true

这里可以直接使用Ribbon 的配置,是因为zuul 里面已经集成了Ribbon的包

10.3 在Ribbon Server添加随机延时

超过500秒就会重试

  1. int millis = new Random().nextInt(3000);
  2. System.out.println("client线程休眠时间:"+millis);
  3. Thread.sleep(millis);

10.4 重启项目

浏览器或Postman 输入localhost:8679/ribbon/user/1
网关Zuul - 图19
可以看到控制台重试的日志
网关Zuul - 图20
这里和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,我们可以自己定义回退机制。

  1. Caused by: java.net.SocketTimeoutException: Read timed out

11.1 新建FallBack 类

实现回退需要实现FallbackProvider接口

  1. /**
  2. * @author xuyisu
  3. * @description fallback
  4. * @date 2020/1/4
  5. */
  6. @Slf4j
  7. @Component
  8. public class ZuulFallBack implements FallbackProvider {
  9. @Override
  10. public String getRoute() {
  11. return "*";
  12. }
  13. @Override
  14. public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
  15. return new ClientHttpResponse() {
  16. @Override
  17. public HttpStatus getStatusCode() throws IOException {
  18. return HttpStatus.OK;
  19. }
  20. @Override
  21. public int getRawStatusCode() throws IOException {
  22. return this.getStatusCode().value();
  23. }
  24. @Override
  25. public String getStatusText() throws IOException {
  26. return this.getStatusCode().getReasonPhrase();
  27. }
  28. @Override
  29. public void close() {
  30. }
  31. @Override
  32. public InputStream getBody() throws IOException {
  33. RequestContext ctx=RequestContext.getCurrentContext();
  34. Throwable throwable = ctx.getThrowable();
  35. if(null!=throwable){
  36. log.error("Zuul发生错误,{}",throwable.getCause().getMessage());
  37. FwResult<String> byteMsg = FwResult.failed(throwable.getCause().getMessage(), "网络或服务异常");
  38. return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());
  39. }
  40. FwResult<String> byteMsg = FwResult.failedMsg("网络或服务异常");
  41. return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());
  42. }
  43. @Override
  44. public HttpHeaders getHeaders() {
  45. HttpHeaders headers=new HttpHeaders();
  46. MediaType mediaType=new MediaType("application", "json", StandardCharsets.UTF_8);
  47. headers.setContentType(mediaType);
  48. return headers;
  49. }
  50. };
  51. }
  52. }
  • getRoute 方法中返回*表示对所有服务进行回退操作,如果只想对某个服务进行回退,
    那么就返回需要回退的服务名称,这个名称是注册到Eureka中的名称
  • ClientHttpResponse 构造回退的内容
  • getStatusCode 返回响应的状态码
  • getStatusText 返回响应状态码对应的文本
  • getBody 返回回退的内容
  • getHeaders 返回响应的请求头信息

    11.2 设置hystrix 超时时间

    假设设置1秒
    1. #回退超时时间
    2. hystrix:
    3. command:
    4. default:
    5. execution:
    6. isolation:
    7. thread:
    8. timeoutInMilliseconds: 1000

    11.3 重启服务

    浏览器或Postman 输入地址localhost:8679/ribbon/user/1,如果想快速看到,可以将fw-cloud-ribbon-server关掉
    网关Zuul - 图21