文档章节

用责任链模式设计拦截器

逅弈逐码
 逅弈逐码
发布于 02/18 09:32
字数 2523
阅读 962
收藏 30

我在 Redant(https://github.com/all4you/redant) 中通过继承 ChannelHandler 实现了拦截器的功能,并且 pipeline 就是一种责任链模式的应用。但是后来我对原本的拦截器进行了重新设计,为什么这样做呢,因为原本的方式是在 ChannelHandler 的基础上实现的,而我们知道 Netty 的数据处理都是基于 ByteBuf 的,这就涉及到引用计数释放的问题,前面的 ChannelHandler 在处理时可以不关心引用计数的问题,而交给最后一个 ChannelHandler 去释放。

但是拦截器的一大特性就是当某个条件不满足时需要中断后面的操作直接返回,所以这就造成了在 pipeline 中某个节点需要释放引用计数,另外一个方面就是原先的设计使用了很多自定义的 ChannelHandler,有的只做了一些简单的工作,所以完全可以对他们进行合并,使代码变得更加精简紧凑。

合并多个 ChannelHandler 是比较简单的,重新设计拦截器相对就复杂一些了。

重新设计拦截器

首先我把原本的前置拦截器和后置拦截器统一成一个拦截器,然后抽象出两个方法,分别表示:前置处理,后置处理,如下图所示:

interceptor.png

默认前置处理的方法返回 true,用户可以根据他们的业务进行覆盖。

这里是定义了一个抽象类,也可以用接口,java 8 开始接口中可以有默认方法实现。

拦截器定义好之后,现在就可以在 ChannelHandler 中加入拦截器的方法调用了,如下图所示:

interceptor-handle.png

当前置方法返回 false 时,直接返回,中断后面的业务逻辑处理,最终会到 finally 中将结果写入 response 中返回给前端。

现在只要实现 InterceptorHandler 中的两个方法就可以了,其实这也很简单,只要获取到所有的 Interceptor 的实现类,然后依次调用这些实现类的前置方法和后置方法就好了,如下图所示:

interceptor-call.png

获取拦截器

现在的重点就是怎样获取到所有的拦截器,首先可以想到的是通过扫描特定路径,找到所有 Interceptor 的实现类,然后将这些实现类加入到一个 List 中即可。

那怎么保证拦截器的执行顺序呢,很简单,只要在加入 List 之前对他们进行排序就可以了。定义一个 @Order 注解来表示排序的顺序,然后用一个 Wrapper 包装类将 Interceptor 和 Order 包装起来,排序到包装类的 List 中,最后再从包装类的 List 中依次取出所有的 Interceptor 就完成了 Interceptor 的排序了。

知道了大致的原理之后,实现起来就很简单了,如下图所示:

get-interceptor.png

但是我们不能每次都通过调用 scanInterceptors() 方法来获取所有的拦截器,如果这样每次都扫描一次的话性能会有影响,所以我们只需要第一次调用一下该方法,然后把结果保存在一个私有的变量中,获取的时候直接读取该变量的值即可,如下图所示:

interceptor-provider.png

自定义拦截器实现类

下面让我们来自定义两个拦截器实现类,来验证下具体的效果。

第一个拦截器,在前置方法中对请求参数进行判断,如果请求参数中有 block=true 的参数,则进行拦截,如下图所示:

block-interceptor.png

第二个拦截器,在后置方法中打印出每次请求的耗时,如下图所示:

performance-interceptor.png

通过 @Order 注解来指定执行的顺序,先执行 BlockInterceptor 再执行 PerformanceInterceptor。

查看效果

现在我们请求 /user/info 这个接口,查看下效果。

首先我们只提交正常的参数,如下图所示:

common-request.png

打印的结果如下图所示:

common-request-effect.png

从打印的结果中可以看到依次执行了:

  • BlockInterceptor 的 preHandle 方法
  • PerformanceInterceptor 的 preHandle方法
  • BlockInterceptor 的 postHandle 方法
  • PerformanceInterceptor 的 postHandle方法

这说明拦截器是按照 @Order 注解进行了排序,然后依次执行的。

然后我们再提交一个 block=true 的参数,再次请求该接口,如下图所示:

block-request.png

可以看到该请求已经被拦截器的前置方法给拦截了,再看下打印的日志,如下图所示:

block-request-effect.png

只打印了 BlockInterceptor 的 preHandler 方法中的部分日志,后面的方法都没有执行,因为被拦截了直接返回了。

存在的问题

到这里已经对拦截器完成了改造,并且也验证了效果,看上去效果还可以。但是有没有什么问题呢?

还真有一个问题:所有的 Interceptor 实现类只要被扫描到了,就会被加入到 List 中去,如果不想应用某一个拦截器这时就做不到了,因为无法对 list 中的值进行动态的更改。

如果我们可以构造一个动态的获取 Interceptor 的 list 构造器,优先从构造器中获取 list,如果用户没有定义构造器时再通过扫描的方式去拿到所有的 Interceptor 这样就完美了。

动态获取 Interceptor 的 list 的方法,可以由用户自定义实现,根据某些规则来确定要不要将某个 Interceptor 加入到 list 中去,这样就把 Interceptor 的实现和使用进行了解耦了。用户可以实现任意多的 Interceptor,但是只根据规则去使用其中的某些 Interceptor。

理清楚了原理之后,就很好实现了,首先定义一个接口,用来构造 Interceptor 的 List,如下图所示:

interceptor-builder.png

有了 InterceptorBuilder 之后,在获取 Interceptor 的时候,就可以先根据 InterceptorBuilder 来获取了,如下图所示:

interceptor-provider-with-builder.png

以下是一个示例的 InterceptorBuilder,具体的可以用户自行扩展设计,如下图所示:

custom-interceptor-builder.png

这样用户只要实现一个 InterceptorBuilder 接口,即可按照自己的意图去组装所有的拦截器。

链式责任链

在 Redant 中实现的拦截器所使用的责任链,其实是通过一个 List 来保存了所有的 Interceptor,那我们通常所说的责任链除了使用 List 来实现外,还可以通过真正的链表结构来实现,Netty 和 Sentinel 中都有这样的实现,下面我来实现一个简单的链式结构的责任链。

责任链的应用已经有很多了,这里不再赘述,假设我们需要对前端提交的请求做以下操作:鉴权,登录,日志记录,通过责任链来做这些处理是非常合适的。

首先定义一个处理接口,如下图所示:

processor.png

通过 List 方式的实现很简单,只需要把每个 Processor 的实现类添加到一个 List 中即可,处理的时候遍历该 List 依次处理,这里不再做具体的描述,感兴趣的可以自行实现。

定义节点

如果是通过链表的形式来实现的话,首先我们需要有一个类表示链表中的某个节点,并且该节点需要有一个同类型的私有变量表示该节点的下个节点,这样就可以实现一个链表了,如下图所示:

abstract-linked-processor.png

定义容器

接着我们需要定义一个容器,在容器中有头,尾两个节点,头结点作为一个空节点,真正的节点将添加到头结点的 next 节点上去,尾节点作为一个指针,用来指向当前添加的节点,下一次添加新节点时,将从尾节点处添加。有了具体的处理逻辑之后,实现起来就很简单了,这个容器的实现如下图所示:

linked-processor-chain.png

定义实现类

下面我们可以实现具体的 Processor 来处理业务逻辑了,只要继承 AbstractLinkedProcessor 即可,如下图所示:

auth-processor.png

其他两个实现类: LoginProcessor ,LogProcessor 类似,这里就不贴出来了。

然后就可以根据规则来组装所需要的 Processor 了,假设我们的规则是需要对请求依次进行:鉴权,登录,日志记录,那组装的代码如下图所示:

linked-processor-chain-test.png

执行该代码,结果如下图所示:

linked-processor-chain-test-result.png

存在的问题

看的仔细的同学可能发现了,在 AuthProcessor 的业务逻辑实现中,除了执行了具体的逻辑代码之外,还调用了一行 super.process(content) 代码,这行代码的作用是调用链表中的下一个节点的 process 方法。但是如果有一天我们在写自己的 Processor 实现类时,忘记调用这行代码的话,会是怎样的结果呢?

结果就是当前节点后面的节点不会被调用,整个链表就像断掉一样,那怎样来避免这种问题的发生呢?其实我们在 AbstractProcessor 中已经实现了 process 方法,该方法就是调用下个节点的 process 方法的。那我们在这个方法触发调用下个节点之前,再抽象出一个用以具体的业务逻辑处理的方法 doProcess ,先执行 doProcess 方法,执行完之后再触发下个节点的 process ,这样就不会出现链表断掉的情况了,具体的实现如下图所示:

fixed-abstract-linked-processor.png

相应的 LinkedProcessorChain 和具体的实现类也要做响应的调整,如下图所示:

fixed-linked-processor-chain.png

fixed-auth-processor.png

重新执行刚刚的测试类,发现结果和之前的一样,至此一个简单的链式责任链完成了。

© 著作权归作者所有

共有 人打赏支持
逅弈逐码
粉丝 41
博文 9
码字总数 34172
作品 0
南京
程序员
私信 提问
加载中

评论(1)

越过山丘so
越过山丘so
感谢楼主分享,借平台在此向大家推荐一个学习交流圈:854601507,群里有总结系统的架构技术体系,大家可以进群下载资料,群里有阿里大牛,也有一线互联网的资深HR,当你遇到的人都是优秀架构师,有一天你也会成为其中的一员,你需要更加明确自己的方向,更加坚定自己的决心。
责任链设计模式应用场景示例(过滤器、拦截器)

责任链设计模式(Chain of Responsibility)的应用有:Java Web中的过滤器链、Struts2中的拦截器栈。 先看一个问题: 给定一个字符串“被就业了:),敏感信息,<script>”,对其中的HTML标记...

凯文加内特
2016/04/19
86
0
Android八门神器(一): OkHttp框架源码解析

HTTP是我们交换数据和媒体流的现代应用网络,有效利用HTTP可以使我们节省带宽和更快地加载数据,Square公司开源的OkHttp网络请求是有效率的HTTP客户端。之前的知识面仅限于框架API的调用,接...

chdee
2018/11/18
0
0
OKHTTP3源码和设计模式(上篇)

本文来探究一下 OkHttp3 的源码和其中的设计思想。 关于 OkHttp3 的源码分析的文章挺多,不过大多还是在为了源码而源码。个人觉得如果读源码不去分析源码背后的设计模式或设计思想,那么读源...

欧阳愠斐
2018/08/21
0
0
国际化信息显示 ,自定义拦截器

四、 国际化信息显示 1、 国际化原理 ? 什么是国际化 ? 同一款软件 可以为不同用户,提供不同语言界面 ---- 国际化软件 需要一个语言资源包(很多properties文件,每个properties文件 针对...

day戴
2014/07/23
0
0
拦截器

一 概述 1 Struts2是框架,封装了很多功能,Struts2里面封装的功能都是在拦截器里面 2 Struts2里面封装了很多的功能,有很多拦截器,不是每次这些拦截器都执行,每次执行默认拦截器 3 Struts...

Bbigbug
2017/10/20
0
0

没有更多内容

加载失败,请刷新页面

加载更多

如果让你写一个消息队列,该如何进行架构设计?

面试题 如果让你写一个消息队列,该如何进行架构设计?说一下你的思路。 面试官心理分析 其实聊到这个问题,一般面试官要考察两块: 你有没有对某一个消息队列做过较为深入的原理的了解,或者...

李红欧巴
今天
4
0
错题

无知的小狼
今天
2
0
PowerShell因为在此系统中禁止执行脚本的解决方法

参考:window系统包管理工具--chocolatey 报错提示: & : 无法加载文件 C:\Users\liuzidong\AppData\Local\Temp\chocolatey\chocInstall\tools\chocolateyInstall.ps1,因为在此系统上禁止运...

近在咫尺远在天涯
今天
3
0
TP5 跨域请求处理

https://blog.csdn.net/a593706205/article/details/81774987 https://blog.csdn.net/wyk9916/article/details/82315700...

15834278076
今天
3
0
深入理解java虚拟机-Java内存区域与内存溢出异常

深入理解java虚拟机 Java内存区域与内存溢出异常 运行时数据区域 程序计数器 线程私有,内存小,是当前线程执行的字节码行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行...

须臾之余
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部