作为乐死不疲的汪界翘楚
本汪每天不是在戏精
就是在戏精的路上了
作为一只纯种哈士奇
玩,就要玩得尽兴而归!
今天与大家一起溜溜,看看 Spring Cloud 网关 ZuulFilter ,从使用到源码的规律
特此感谢:Mikey Cohen 老哥
FilterLoader 和 ZuulFilter 、IZuulFilter
Spring Cloud 网关这块儿,都是这位老哥的杰作。
嗷呜嗷呜!!:
通过这篇博客,我们能学的什么:
1、了解Zuul 拦截器的组成结构和执行顺序。
2、学到3种常用的拦截器使用方式,身份校验拦截器,限流拦截器,服务器响应数据统计拦截器。
3、弄懂拦截器底层源码的执行顺序和各部分是如何协同作用的。
4、学到 FilterFactory 拦截器工厂的实际使用案例。
那么,和本汪一起去看一下吧,走你!
Here are the core parts below:
1、Zuul 是一个API Gateway 服务器,是Netflix基于JVM的路由器和服务器端负载均衡器,本质上是一个 Web Servlet 应用。
2、Zuul 提供了动态路由、监控等服务,这些功能的实现核心是一系列的 filter。
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 等。
1、四个主要的抽象方法
这四个方法,是位于不同的类中哦!
其中, filterType() 和 filterOrder() 两个抽象方法,位于 com.netflix.zuul.ZuulFilter
中,而shouldFilter() 和 run() 则位于com.netflix.zuul.IZuulFilter
中,如图:
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 这两个类型,是我们使用最多的,因此我们通常把他们单独拿出来,构建抽象类 AbstractPreZuulFilter
和 AbstractPostZuulFilter
。在其中实现 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;
}
}
程序启动后,zuul会定期扫描Filter文件的存放这些目录
1、FilterFileManager
负责管理目录轮询,监测Filter文件是否发生更改或有新的Groovy过滤器
(1)轮询间隔和目录位置在类的初始化中指定
(2)启动一个线程,按照默认指定的间隔时间,进行轮询监控。
(3)调用public File getDirectory(String sPath)
获取文件路径,再执行List
返回获取到的文件列表,接着调用void processGroovyFiles(List
对获取到的Groovy 文件进行处理,processGroovyFiles()
内部调用public boolean putFilter(File file) throws Exception
这将从一个文件中读取ZuulFilter源代码,编译它,并将它添加到当前过滤器的列表中。如果文件中的筛选器成功读取、编译、验证并添加到Zuul,则返回 true。
(4)筛选的过程很简单,一个ConcurrentHashMap
用来存放上次加载过的记录,验证时,用文件名作为 Key 去取就好。
如果该文件没有注册过,就对文件进行编译读取,接着再注册,并存放入filterClassLastModified
中做为记录,以便下次比较就好了。
同时注意, 此处使用了工厂模式。 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
002#. 拦截器工厂的默认具体实现类:
不能再往外扯了,所以类的装载机制这块儿,本汪就不在这篇文章里说了,不知怎么得,感觉有点面试的感觉,越走越深,不加把控估计能聊到内存屏障和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
便是在这个类中。
·
(2)ZuulServlet
继承自HttpServlet
,核心zuulservlet,初始化和编排zuulFilter执行,核心方法四个,分别对应“post”、“route”、“pre”、 “error”的执行:
void postRoute()
、void route()
、void preRoute()
、void error
。其内部皆是调用了ZuulRunner
的相应方法。
.
(3)ZuulRunner
此类将servlet请求和响应初始化到RequestContext中,并包装FilterProcessor调用到preRoute()
、route()
、postRoute()
和error()
方法。也就是说,此处会调用FilterProcessor
中对应的方法,进行具体的执行。
.
(4)FilterProcessor
这是执行过滤器的最终执行类。这个类中,
public Object runFilters(String sType) throws Throwable
方法,将运行filterType类型的所有筛选器/在筛选器中使用此方法将按类型运行自定义筛选器。
3、另外需要知道的是:类似 JIT的一次编译,快速执行,ZuulFilter 的加载过程,也是这样。第一次执行后,会将全部拦截器信息存储在临时ConcurrentHashMap
中,之后都是先从这儿读取,没有再执行加载。
.
(1)当我们第一次进行网关请求时,会第一次调用FilterLoader
中的getFiltersByType
方法,此时的 list 还是空的,
List<ZuulFilter> list = hashFiltersByType.get(filterType);//list is null
.
(2)在执行getFiltersByType()
方法时,会从bean 工厂中获取已经注册过的继承自 ZuulFilter 的全部实现类。还记得下面的 @Component 注解吗?我们就是通过他进行实例注册的哦。
.
(3)FilterLoader
中,通过ConcurrentHashMap<拦截器类型,List<拦截器>>
对新增和已有拦截器进行维护。
每次我们进行请求走“pre”拦截器时,都会从FilterRegistry
中读取全部已注册的全部拦截器对象,再通过filterType
筛选出类型为“pre”的全部拦截器。之后,按照我们设置的 filterOrder
的值,从小到大的顺序进行拦截器的执行。
到此为止,斯以为是理解了80%了,不知看到这句话的小伙伴有几人?快来留言,签个到吧!!