咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)

作为乐死不疲的汪界翘楚
本汪每天不是在戏精
就是在戏精的路上了
作为一只纯种哈士奇
玩,就要玩得尽兴而归!
今天与大家一起溜溜,看看 Spring Cloud 网关 ZuulFilter ,从使用到源码的规律

特此感谢:Mikey Cohen 老哥
FilterLoader 和 ZuulFilter 、IZuulFilter
Spring Cloud 网关这块儿,都是这位老哥的杰作。
嗷呜嗷呜!!:
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第1张图片

前言:

通过这篇博客,我们能学的什么:

1、了解Zuul 拦截器的组成结构和执行顺序。
2、学到3种常用的拦截器使用方式,身份校验拦截器,限流拦截器,服务器响应数据统计拦截器。
3、弄懂拦截器底层源码的执行顺序和各部分是如何协同作用的。
4、学到 FilterFactory 拦截器工厂的实际使用案例。

那么,和本汪一起去看一下吧,走你!

一、开篇有益(5%的小伙伴到此为止)

Here are the core parts below:

1、Zuul 是一个API Gateway 服务器,是Netflix基于JVM的路由器和服务器端负载均衡器,本质上是一个 Web Servlet 应用。
2、Zuul 提供了动态路由、监控等服务,这些功能的实现核心是一系列的 filter。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第2张图片
Spring Cloud ZuulFilter consist of the following core parts:

(1)pre filters 前置过滤器,在请求到达路由之前调用,进行身份验证,在集群中选择微服务,记录调试信息等。

(2)routing filters 路由过滤器 将请求路由到微服务,用于构建发送给微服务请求。

(3)post filters 在请求路由到微服务以后执行,可以为响应添加标准的 HTTP header, 收集统一信息和指标,将响应从微服务发送给客户端。

(4)error fiilters 任何阶段执行发生了错误,都会调用。

(5)custom filters 我们为了满足一些特定的需求,而自己定义的过滤器。例如 TokenFilter、RateLimiterFilter 等。

二、浅尝辄止,会用就好(剩余80%的小伙伴到此为止):

1、四个主要的抽象方法

咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第3张图片
这四个方法,是位于不同的类中哦!
其中, filterType() 和 filterOrder() 两个抽象方法,位于 com.netflix.zuul.ZuulFilter中,而shouldFilter() 和 run() 则位于com.netflix.zuul.IZuulFilter中,如图:
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第4张图片
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第5张图片

2.我们以三个 Filter 的实际使用场景为例,展示 Filter 的实际使用

As mentioned above, self define zuul filters have to extend ZuulFilter. Therefore, I'll focus on these four methods in this ZuulFilter overview.

(1)首先,我们需要编写一个基础的过滤器抽象类AbstractZuulFilter,它直接基础自 com.netflix.zuul.ZuulFilter, 我们在这儿对shouldFilter()run()提供基础的实现。

(2)从设计的角度上,pre、 post 这两个类型,是我们使用最多的,因此我们通常把他们单独拿出来,构建抽象类 AbstractPreZuulFilterAbstractPostZuulFilter。在其中实现 filterType()

(3)当我们需要实现自定义的拦截器时,可以根据他在请求和路由前后的位置,来决定他是继承自 AbstractPreZuulFilter 还是 AbstractPostZuulFilter

当我们有多个 pre 类型过滤器时,我们有时可能需要让他们按照一定的顺序去执行,那么我们可以通过设置filterOrder的值,来使他们按顺序执行。

因此,filterOrder()是在我们具体的自定义 Filters 中才给出具体值的。

嗷呜嗷呜!!

(1)这是最基础的:AbstractZuulFilter直接继承自ZuulFilter

package com.tencent.coupon.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

/**
 * Base abstract class for ZuulFilters. The base class defines abstract methods to define:
 * filterType() - to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering,
 * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling.
 * 

* filterOrder() must also be defined for a filter. Filters may have the same filterOrder if precedence is not * important for a filter. filterOrders do not need to be sequential. *

* ZuulFilters may be disabled using Archius Properties. *

* By default ZuulFilters are static; they don't carry state. This may be overridden by overriding the isStaticFilter() property to false * * @author Husky Yue * Date: 5/11/20 * Time: 9:59 PM */ public abstract class AbstractZuulFilter extends ZuulFilter { /** * The Request Context holds request, response, state information and data for ZuulFilters to access and share. * The RequestContext lives for the duration of the request and is ThreadLocal. * extensions of RequestContext can be substituted by setting the contextClass. * Most methods here are convenience wrapper methods; the RequestContext is an extension of a ConcurrentHashMap */ RequestContext context; private final static String NEXT = "next"; @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return (boolean) ctx.getOrDefault(NEXT, true); } @Override public Object run() throws ZuulException { context = RequestContext.getCurrentContext(); return cRun(); } protected abstract Object cRun(); /** * When the request fails, the response body is stitched * together with the failure information and returned. */ Object fail(int code, String msg) { context.set(NEXT, false); context.setSendZuulResponse(false); context.getResponse().setContentType("text/html;charset=UTF-8"); context.setResponseStatusCode(code); context.setResponseBody(String.format("{\"result\": \"%s!\"}", msg)); return null; } /** *Additional interceptions in "next" are allowed to process * when the request is successful. */ Object success() { context.set(NEXT, true); return null; } }

嗷呜嗷呜!!

(2)前置 pre 过滤器都可继承该类:AbstractPreZuulFilter继承自AbstractZuulFilter

package com.tencent.coupon.filter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

/**
 * If the interceptor you want to implement is located
 * before the request reaches the router, you can inherit this class directly
 * @author Husky Yue
 *         Date: 5/11/20
 *         Time: 10:10 PM
 */
public abstract class AbstractPreZuulFilter extends AbstractZuulFilter{
     
    @Override
    public String filterType() {
     
        return FilterConstants.PRE_TYPE;
    }
}

(3)后置 post 过滤器都可继承该类:AbstractPostZuulFilter继承自AbstractZuulFilter

package com.tencent.coupon.filter;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

/**
 *If you want to implement an interceptor that executes 
 *after the request is routed to the microservice, you can inherit this class directly
 * @author Husky Yue
 *         Date: 5/11/20
 *         Time: 10:14 PM
 */
public abstract class AbstractPostZuulFilter extends AbstractZuulFilter{
     
    @Override
    public String filterType() {
     
        return FilterConstants.POST_TYPE;
    }
}

3、下面为实际应用场景,嗷呜嗷呜!!

实际使用情景(1):TokenFilter身份验证前置过滤器
校验token 是否为空,该拦截器在请求到达路由之前调用,进行身份验证,通过校验才允许执行之后的拦截器;否则,直接进行响应的返回。

package com.tencent.coupon.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;

/**
 * 

校验请求中的 Token

* @author Husky Yue * Date: 5/11/20 * Time: 10:20 PM */
@Component public class TokenFilter extends AbstractPreZuulFilter { private static final Logger LOG = LoggerFactory.getLogger(TokenFilter.class); @Override protected Object cRun() { HttpServletRequest request = context.getRequest(); LOG.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString())); Object token = request.getParameter("token"); if (null == token) { LOG.error("error: token is empty"); return fail(401, "error: token is empty"); } return success(); } @Override public int filterOrder() { return 1; } }

嗷呜嗷呜!!

实际使用情景(2):RateLimiterFilter限流前置过滤器
高并发下的限流过滤器,可以根据实际情形,设置流量带宽。本汪在“咖啡汪日志——实际开发中如何避免缓存穿透和缓存雪崩(代码示例实际展示)”一文中,还有介绍其他系统防护方式哦。

package com.tencent.coupon.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.RateLimiter;

import javax.servlet.http.HttpServletRequest;

 /**
 * 

限流过滤器

* @author Husky Yue * Date: 5/11/20 * Time: 10:28 PM */
@Component @SuppressWarnings("all") public class RateLimiterFilter extends AbstractPreZuulFilter{ private static final Logger LOG = LoggerFactory.getLogger(RateLimiterFilter.class); /** 每秒可以获取到两个令牌 */ RateLimiter rateLimiter = RateLimiter.create(2.0); @Override protected Object cRun() { HttpServletRequest request = context.getRequest(); if (rateLimiter.tryAcquire()) { LOG.info("get rate token success"); return success(); } else { LOG.error("rate limit: {}", request.getRequestURI()); return fail(402, "error: rate limit"); } } /** * filterOrder() must also be defined for a filter. Filters may have the same filterOrder if precedence is not * important for a filter. filterOrders do not need to be sequential. * * @return the int order of a filter */ @Override public int filterOrder() { return 2; } }

嗷呜嗷呜!!

实际使用情景(3):PreRequestFilter前置过滤器 和 AccessLogFilter后置过滤器
通过在进入服务前在 RequestContext 中存储时间戳,在服务返回之后拦截读取 服务的响应时间,进行日志打印。利用日志中记录的响应时间绘制 echart 图,可以对系统的负载和响应时间进行监控。(此处仅为示例,为得是展示拦截器的环绕情形。通常大型系统在灰度测试和正式环境都是采用异步日志记录的,同时需要注意异步日志撑爆内存,异步日志出现丢失,异步日志出现阻塞等情况

(1)记录请求进入服务时间:

package com.tencent.coupon.filter;

import org.springframework.stereotype.Component;

/**
 * 

在过滤器中存储客户端发起请求的时间戳

* @author Husky Yue * Date: 5/11/20 * Time: 10:40 PM */
@Component public class PreRequestFilter extends AbstractPreZuulFilter{ @Override protected Object cRun() { context.set("startTime", System.currentTimeMillis()); return success(); } /** * filterOrder() must also be defined for a filter. Filters may have the same filterOrder if precedence is not * important for a filter. filterOrders do not need to be sequential. * * @return the int order of a filter */ @Override public int filterOrder() { return 0; } }

(2)记录服务处理请求用时

package com.tencent.coupon.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 

在日志中记录服务处理请求的用时的拦截器

* @author Husky Yue * Date: 5/11/20 * Time: 10:48 PM */
@Component public class AccessLogFilter extends AbstractPostZuulFilter{ public static final Logger LOG = LoggerFactory.getLogger(AccessLogFilter.class); @Override protected Object cRun() { HttpServletRequest request = context.getRequest(); // 从 PreRequestFilter 中获取设置的请求时间戳 Long startTime = (Long) context.get("startTime"); String uri = request.getRequestURI(); long duration = System.currentTimeMillis() - startTime; // 从网关通过的请求都会打印日志记录: uri + duration LOG.info("uri: {}, duration: {}", uri, duration); return success(); } @Override public int filterOrder() { return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; } }

日志效果如下:
在这里插入图片描述

三、不甘寂寞,刨根揭底(剩余5%的小伙伴到此为止):

程序启动后,zuul会定期扫描Filter文件的存放这些目录
1、FilterFileManager 负责管理目录轮询,监测Filter文件是否发生更改或有新的Groovy过滤器
(1)轮询间隔和目录位置在类的初始化中指定
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第6张图片
(2)启动一个线程,按照默认指定的间隔时间,进行轮询监控。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第7张图片
(3)调用public File getDirectory(String sPath)获取文件路径,再执行List getFiles()返回获取到的文件列表,接着调用void processGroovyFiles(List aFiles) throws Exception, InstantiationException, IllegalAccessException 对获取到的Groovy 文件进行处理,processGroovyFiles()内部调用public boolean putFilter(File file) throws Exception这将从一个文件中读取ZuulFilter源代码,编译它,并将它添加到当前过滤器的列表中。如果文件中的筛选器成功读取、编译、验证并添加到Zuul,则返回 true。

(4)筛选的过程很简单,一个ConcurrentHashMap filterClassLastModified用来存放上次加载过的记录,验证时,用文件名作为 Key 去取就好。
在这里插入图片描述
如果该文件没有注册过,就对文件进行编译读取,接着再注册,并存放入filterClassLastModified中做为记录,以便下次比较就好了。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第8张图片
同时注意, 此处使用了工厂模式。 FILTER_FACTORY.newInatance(class)是典型的工厂模式获取实例的方法。
既然说到这儿了,本汪就多句嘴:Factory Pattern ,define an interface for creating an object,but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. FilterFactory 为抽象事物类负责定义事物的共性,实现对事物最抽象的定义。 DefaultFilterFactory 为抽象创建类,负责具体的实现。
youFilterFactory 即拦截器工厂 及其默认实现类DefaultFilterFactory构造拦截器 instance。

001 #.工厂模式,拦截器工厂接口:FilterFactory
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第9张图片
002#. 拦截器工厂的默认具体实现类:
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第10张图片
不能再往外扯了,所以类的装载机制这块儿,本汪就不在这篇文章里说了,不知怎么得,感觉有点面试的感觉,越走越深,不加把控估计能聊到内存屏障和cpu 的指令集,O(∩_∩)O哈哈~

003#. 在FilterLoader中,直接进行静态加载。

static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();

实例化时:

ZuulFilter filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);

2、除了FilterFileManager, 还有四个核心执行类:
.

(1)FilterLoader 原类上的注释是这样的:这个类是Zuul的核心类之一。它编译、从文件加载,并检查源代码是否更改。它还通过filterType保存ZuulFilters。
上面提到的public boolean putFilter(File file) throws Exception便是在这个类中。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第11张图片

·
(2)ZuulServlet 继承自HttpServlet,核心zuulservlet,初始化和编排zuulFilter执行,核心方法四个,分别对应“post”、“route”、“pre”、 “error”的执行:
void postRoute()void route()void preRoute()void error。其内部皆是调用了ZuulRunner的相应方法。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第12张图片
.

(3)ZuulRunner此类将servlet请求和响应初始化到RequestContext中,并包装FilterProcessor调用到preRoute()route()postRoute()error()方法。也就是说,此处会调用FilterProcessor中对应的方法,进行具体的执行。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第13张图片
.
(4)FilterProcessor这是执行过滤器的最终执行类。这个类中,
public Object runFilters(String sType) throws Throwable方法,将运行filterType类型的所有筛选器/在筛选器中使用此方法将按类型运行自定义筛选器。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第14张图片

3、另外需要知道的是:类似 JIT的一次编译,快速执行,ZuulFilter 的加载过程,也是这样。第一次执行后,会将全部拦截器信息存储在临时ConcurrentHashMap>中,之后都是先从这儿读取,没有再执行加载。
.
(1)当我们第一次进行网关请求时,会第一次调用FilterLoader中的getFiltersByType方法,此时的 list 还是空的,

 List<ZuulFilter> list = hashFiltersByType.get(filterType);//list is null

.
(2)在执行getFiltersByType()方法时,会从bean 工厂中获取已经注册过的继承自 ZuulFilter 的全部实现类。还记得下面的 @Component 注解吗?我们就是通过他进行实例注册的哦。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第15张图片
.
(3)FilterLoader中,通过ConcurrentHashMap<拦截器类型,List<拦截器>>对新增和已有拦截器进行维护。

每次我们进行请求走“pre”拦截器时,都会从FilterRegistry中读取全部已注册的全部拦截器对象,再通过filterType 筛选出类型为“pre”的全部拦截器。之后,按照我们设置的 filterOrder的值,从小到大的顺序进行拦截器的执行。
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第16张图片
咖啡汪日志 —— 一文看懂Spring Cloud 网关ZuulFilter的使用和源码(包括FilterFactory 的讲解)_第17张图片
到此为止,斯以为是理解了80%了,不知看到这句话的小伙伴有几人?快来留言,签个到吧!!

你可能感兴趣的:(技术干货,spring,filter,设计模式,java)