Spring Cloud入门教程(六):API服务网关(Zuul) 下

上一篇:《Spring Cloud入门教程(五):API服务网关(Zuul) 上》

本人和同事撰写的《Spring Cloud微服务架构开发实战》一书已在京东、当当等书店上架,大家可以点击这里前往购买,多谢大家支持和捧场!


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命令进行包装,所以这类请求也就没有线程隔离和断路器保护。

你可以到这里下载本篇的代码。

下一篇:《Spring Cloud入门教程(七):分布式链路跟踪(Sleuth)》

推荐阅读更多精彩内容