Zuul给我们的第一印象通常是这样:它包含了对请求的路由和过滤两个功能,其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础。过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。然而实际上,路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的。其中,路由映射主要是通过PRE类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址。而请求转发的部分则是由Route类型的过滤器来完成,对PRE类型过滤器获得的路由地址进行转发。所以,过滤器可以说是Zuul实现API网关功能最重要的核心部件,每一个进入Zuul的请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。

1. 过滤器简介

1.1 过滤器特性

Zuul过滤器的关键特性有:

  • Type: 定义在请求执行过程中何时被执行;
  • Execution Order: 当存在多个过滤器时,用来指示执行的顺序,值越小就会越早执行;
  • Criteria: 执行的条件,即该过滤器何时会被触发;
  • Action: 具体的动作。

过滤器之间并不会直接进行通信,而是通过RequestContext来共享信息,RequestContext是线程安全的。

对应上面Zuul过滤器的特性,我们在实现一个自定义过滤器时需要实现的方法有:

/**
 * Zuul Pre-Type Filter
 *
 * @author CD826(CD826Dong@gmail.com)
 * @since 1.0.0
 */
public class PreTypeZuulFilter extends ZuulFilter {
    protected Logger logger = LoggerFactory.getLogger(PreTypeZuulFilter.class);

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        this.logger.info("This is pre-type zuul filter.");
        return null;
    }
}

其中:

  • filterType()方法是该过滤器的类型;
  • filterOrder()方法返回的是执行顺序;
  • shouldFilter()方法则是判断是否需要执行该过滤器;
  • run()则是所要执行的具体过滤动作。

1.2 过滤器类型

Zuul中定义了四种标准的过滤器类型,这些过滤器类型对应于请求的典型生命周期。

  • PRE过滤器: 在请求被路由之前调用, 可用来实现身份验证、在集群中选择请求的微服务、记录调试信息等;
  • ROUTING过滤器: 在路由请求时候被调用;
  • POST过滤器: 在路由到微服务以后执行, 可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等;
  • ERROR过滤器: 在处理请求过程时发生错误时被调用。

Zuul过滤器的类型其实也是Zuul过滤器的生命周期,通过下面这张图来了解它们的执行过程。

 
Zuul-Filter-010

除了上面给出的四种默认的过滤器类型之外,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

1.3 自定义过滤器示例代码

笔者自己没有单独构建一个过滤器示例的场景,我们看一下官方给出的几个示例。

PRE类型示例

public class QueryParamPreFilter extends ZuulFilter {
    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
            && !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (request.getParameter("foo") != null) {
            // put the serviceId in `RequestContext`
            ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
        }
        return null;
    }
}

这个是官方给出的一个示例,从请求的参数foo中获取需要转发到的服务Id。当然官方并不建议我们这么做,这里只是方便给出一个示例而已。

ROUTE类型示例

public class OkHttpRoutingFilter extends ZuulFilter {
    @Autowired
    private ProxyRequestHelper helper;

    @Override
    public String filterType() {
        return ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return RequestContext.getCurrentContext().getRouteHost() != null &&             RequestContext.getCurrentContext().sendZuulResponse();
    }

    @Override
    public Object run() {
        OkHttpClient httpClient = new OkHttpClient.Builder()
            // customize
            .build();

        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();

        String method = request.getMethod();

        String uri = this.helper.buildZuulRequestURI(request);

        Headers.Builder headers = new Headers.Builder();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            Enumeration<String> values = request.getHeaders(name);

            while (values.hasMoreElements()) {
                String value = values.nextElement();
                headers.add(name, value);
            }
        }

        InputStream inputStream = request.getInputStream();

        RequestBody requestBody = null;
        if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
            MediaType mediaType = null;
            if (headers.get("Content-Type") != null) {
                mediaType = MediaType.parse(headers.get("Content-Type"));
            }
            requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
        }

        Request.Builder builder = new Request.Builder()
            .headers(headers.build())
            .url(uri)
            .method(method, requestBody);

        Response response = httpClient.newCall(builder.build()).execute();

        LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
        for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
            responseHeaders.put(entry.getKey(), entry.getValue());
        }

        this.helper.setResponse(response.code(), response.body().byteStream(),          responseHeaders);
        context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
        return null;
    }
}

这个示例是将HTTP请求转换为使用OkHttp3进行请求,并将服务端的返回转换成Servlet的响应。

注意: 官方说这仅仅是一个示例,功能不一定正确。

POST类型示例

public class AddResponseHeaderFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletResponse servletResponse = context.getResponse();        servletResponse.addHeader("X-Foo", UUID.randomUUID().toString());
        return null;
    }
}

这个示例很简单就是返回的头中增加一个随机生成X-Foo

1.4 禁用过滤器

只需要在application.properties(或yml)中配置需要禁用的filter,格式为:zuul.[filter-name].[filter-type].disable=true。如:

zuul.FormBodyWrapperFilter.pre.disable=true

1.5 关于Zuul过滤器Error的一点补充

当Zuul在执行过程中抛出一个异常时,error过滤器就会被执行。而SendErrorFilter只有在RequestContext.getThrowable()不为空的时候才会执行。它将错误信息设置到请求的javax.servlet.error.*属性中,并转发Spring Boot的错误页面。

Zuul过滤器实现的具体类是ZuulServletFilter,其核心代码如下:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
        try {
            preRouting();
        } catch (ZuulException e) {
            error(e);
            postRouting();
            return;
        }

        // Only forward onto to the chain if a zuul response is not being sent
        if (!RequestContext.getCurrentContext().sendZuulResponse()) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        try {
            routing();
        } catch (ZuulException e) {
            error(e);
            postRouting();
            return;
        }
        try {
            postRouting();
        } catch (ZuulException e) {
            error(e);
            return;
        }
    } catch (Throwable e) {
        error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
    } finally {
        RequestContext.getCurrentContext().unset();
    }
}

从这段代码中可以看出,error可以在所有阶段捕获异常后执行,但是如果post阶段中出现异常被error处理后则不再回到post阶段执行,也就是说需要保证在post阶段不要有异常,因为一旦有异常后就会造成该过滤器后面其它post过滤器将不再被执行。

一个简单的全局异常处理的方法是: 添加一个类型为error的过滤器,将错误信息写入RequestContext,这样SendErrorFilter就可以获取错误信息了。代码如下:

public class GlobalErrorFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return ERROR_TYPE;
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = context.getThrowable();
        this.logger.error("[ErrorFilter] error message: {}", throwable.getCause().getMessage());
        context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        context.set("error.exception", throwable.getCause());
        return null;
    }
}

2. @EnableZuulServer VS. @EnableZuulProxy

Zuul为我们提供了两个主应用注解: @EnableZuulServer@EnableZuulProxy,其中@EnableZuulProxy包含@EnableZuulServer的功能,而且还加入了@EnableCircuitBreaker@EnableDiscoveryClient。当我们需要运行一个没有代理功能的Zuul服务,或者有选择的开关部分代理功能时,那么需要使用 @EnableZuulServer 替代 @EnableZuulProxy。 这时候我们可以添加任何 ZuulFilter类型实体类都会被自动加载,这和上一篇使用@EnableZuulProxy是一样,但不会自动加载任何代理过滤器。

2.1 @EnableZuulServer默认过滤器

当我们使用@EnableZuulServer时,默认所加载的过滤器有:

2.1.1 PRE类型过滤器

  • ServletDetectionFilter

该过滤器是最先被执行的。其主要用来检查当前请求是通过SpringDispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。判断结果会保存在isDispatcherServletRequest中,值类型为布尔型。

  • FormBodyWrapperFilter

该过滤器的目的是将符合要求的请求体包装成FormBodyRequestWrapper对象,以供后续处理使用。

  • DebugFilter

PRE类型过滤器。当请求参数中设置了debug参数时,该过滤器会将当前请求上下文中的RequestContext.setDebugRouting()RequestContext.setDebugRequest()设置为true,这样后续的过滤器可以根据这两个参数信息定义一些debug信息,当生产环境出现问题时,我们就可以通过增加该参数让后台打印出debug信息,以帮助我们进行问题分析。对于请求中的debug参数的名称,我们可以通过zuul.debug.parameter进行自定义。

2.1.2 ROUTE类型过滤器

  • SendForwardFilter

该过滤器只对请求上下文中存在forward.to(FilterConstants.FORWARD_TO_KEY)参数的请求进行处理。即处理之前我们路由规则中forward的本地跳转。

2.1.3 POST类型过滤器

  • SendResponseFilter

该过滤器就是对代理请求所返回的响应进行封装,然后作为本次请求的相应发送回给请求者。

2.1.4 Error类型过滤器

  • SendErrorFilter

该过滤器就是判断当前请求上下文中是否有异常信息(RequestContext.getThrowable()不为空),如果有则默认转发到/error页面,我们也可以通过设置error.path来自定义错误页面。

2.2 @EnableZuulProxy默认过滤器

@EnableZuulProxy则在上面的基础上增加以下过滤器:

2.2.1 PRE类型过滤器

  • PreDecorationFilter

该过滤器根据提供的RouteLocator确定路由到的地址,以及怎样去路由。该路由器也可为后端请求设置各种代理相关的header。

2.2.2 ROUTE类型过滤器

  • RibbonRoutingFilter

该过滤器会针对上下文中存在serviceId(可以通过RequestContext.getCurrentContext().get(“serviceId”)获取)的请求进行处理,使用Ribbon、Hystrix和可插拔的HTTP客户端发送请求,并将服务实例的请求结果返回。也就是之前所说的只有当我们使用serviceId配置路由规则时Ribbon和Hystrix方才生效。

  • SimpleHostRoutingFilter

该过滤器检测到routeHost参数(可通过RequestContext.getRouteHost()获取)设置时,就会通过Apache HttpClient向指定的URL发送请求。此时,请求不会使用Hystrix命令进行包装,所以这类请求也就没有线程隔离和断路器保护。

原文地址:https://www.jianshu.com/p/f786a11a2def

【Dalston】【第六章】API服务网关(Zuul) 下的更多相关文章

  1. 一起来学Spring Cloud | 第六章:服务网关 ( Zuul)

    本章节,我们讲解springcloud重要组件:微服务网关Zuul.如果有同学从第一章看到本章的,会发现我们已经讲解了大部分微服务常用的基本组件. 已经讲解过的: 一起来学Spring Cloud | ...

  2. 【Dalston】【第五章】API服务网关(Zuul) 上

    微服务场景下,每一个微服务对外暴露了一组细粒度的服务.客户端的请求可能会涉及到一串的服务调用,如果将这些微服务都暴露给客户端,那么客户端需要多次请求不同的微服务才能完成一次业务处理,增加客户端的代码复 ...

  3. springcloud(十六):服务网关 zuul 快速入门

    服务网关是微服务架构中一个不可或缺的部分.通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由.均衡负载功能之外,它还具备了权限控制等功能.Spring Cloud Netflix中 ...

  4. Spring Cloud(六):服务网关zuul

    通过前面几篇文章的介绍,Spring Cloud微服务架构可通过Eureka实现服务注册与发现,通过Ribbon或Feign来实现服务间的负载均衡调用,通过Hystrix来为服务调用提供服务降级.熔断 ...

  5. Spring Cloud实战之初级入门(六)— 服务网关zuul

    目录 1.环境介绍 2.api网关服务 2.1 创建工程 2.3 api网关中使用token机制 2.4 测试 2.5 小结 3.一点点重要的事情 1.环境介绍 好了,不知不觉中我们已经来到了最后一篇 ...

  6. 白话SpringCloud | 第十一章:路由网关(Zuul):利用swagger2聚合API文档

    前言 通过之前的两篇文章,可以简单的搭建一个路由网关了.而我们知道,现在都奉行前后端分离开发,前后端开发的沟通成本就增加了,所以一般上我们都是通过swagger进行api文档生成的.现在由于使用了统一 ...

  7. Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

    技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢. 当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问 ...

  8. API服务网关(Zuul)

    技术背景 前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢. 当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问 ...

  9. SpringCloud-微服务网关ZUUL(六)

    前言:前面说过,由于微服务过多,可能某一个小业务就需要调各种微服务的接口,不可避免的就会需要负载均衡和反向代理了,以确保ui不直接与所有的微服务接口接触,所以我们需要使用一个组件来做分发,跨域等各种请 ...

随机推荐

  1. html5-相对定位

    *{    margin: 0px;    padding: 0px;}div{    width: 300px;    height: 300px;}#div1{    background: rg ...

  2. Java多线程-----创建线程的几种方式

       1.继承Thread类创建线程 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体 创建Thread子类的实例,也就是创建 ...

  3. 【Redis学习之五】Redis数据类型:列表和散列

    环境 虚拟机:VMware 10 Linux版本:CentOS-6.5-x86_64 客户端:Xshell4 FTP:Xftp4 jdk8 redis-2.8.18 一.列表 基于Linked Lis ...

  4. Linux基础命令---间歇执行命令watch

    watch watch指令可以间歇性的执行程序,将输出结果以全屏的方式显示,默认是2s执行一次.watch将一直运行,直到被中断. 此命令的适用范围:RedHat.RHEL.Ubuntu.CentOS ...

  5. Hadoop学习笔记之三:DataNode

    DataNode对ClientDatanodeProtocol.InterDatanodeProtocol两个协议接口进行了实现,通过ipc::Server向Client.其它DN提供RPC服务(参见 ...

  6. Java中在java.sql.Date的系统时间上加上30天并写入oracle

    在java.sql.Date的系统时间上加上30天,并写入oracle 思路:通过 Calendar.getInstance() 获得对象,然后 add() 方法添加 时间,再通过 new java. ...

  7. web前端学习路线:HTML5教程之前端模块化开发

    1. 命名冲突 首先从一个简单的习惯开始. 由于以前一直做 JavaEE 开发的缘故,在 JavaScript 开发中,我已经习惯将项目中的一些通用功能抽象出来,形成一个个的独立函数,以便于实现代码复 ...

  8. runltp&ltp-pan

  9. The logback manual #02# Architecture

    索引 Logback's architecture Logger, Appenders and Layouts Effective Level(有效等级)又名Level Inheritance Ret ...

  10. PYQT5 + PYCHARM

    PYQT5 C:\Users\xxx\AppData\Local\Programs\Python\Python37\Lib\site-packages\pyqt5_tools\designer.exe ...