再谈pipeline-filter模式

本文结合最近我正在实现的一个基于RabbitMQ的消息总线上所走的弯路来谈谈设计层面上的责任链模式以及架构层面上的pipeline-filter模式。写这篇文章的另一个目的是为了纠正我之前针对pipeline-filter模式写的一篇博文:《pipeline-filter模式变体之尾循环》,如果你想看看我之前为什么要那么做,你可以先看看那篇文章,不过无论看不看都不影响这篇文章的行文。

消息总线需要扩展性

目前这个消息总线实现了produce/consume、request/response、publish/subscribe、broadcast这几种消息通信场景。这些场景中都涉及到消息的处理。

我想实现一种基于plugin的消息处理。它们需要是粒度较细的,并可在各种消息通信模式之间可复用、易于扩展的,并且基于配置文件可以自动将这些不同的plugin串联成一个pipeline。这种模式称之为责任链模式或pipeline-filter模式(如果你区分得严谨,那么可能会将pipeline-filter划归为架构模式,见POSA 卷4),如果你做过java web开发,你总是容易将它跟filter联系起来。没错,filter的这种模式其实就是责任链模式。

两种模式的主流认识

其实通常我们谈责任链跟pipeline-filter,大部分的注意力都集中在“进”的意识上:数据(通常被封装在一个上下文对象中)在调用链上被每个filter依次处理,向前推进。但java web技术里的filter的实现却同时关注了“进与退”:

这是多种原因共同作用的结果:

  1. http有请求也有响应:数据的处理不是单向的,是个闭合的回路
  2. 它的上下文其实是两个对象:HttpServletRequest、HttpServletResponse,进的时候关注HttpServletRequest对象,退的时候关注HttpServletResponse对象,分工明确,互不干扰
  3. 实现这种filter-chain的做法通常都是递归调用;而递归调用在方法执行上涉及到入栈跟出栈的过程。临界点就是方法内部对该递归方法的调用(见上面的chain.doFilter。调用之前的代码可看做入栈,会被先调用;调用之后的代码可被看做出栈,会在所有入栈完成后再依次出栈时被调用)。而图中最后的servlet的Service可以看作是该递归的break point,执行完成之后,将会开始退的(处理HttpServletResponse)流程

这里有个简单的filter示例:它先判断http请求头中是否包含了支持gzip压缩的信息:如果没包含就直接进入下一步,如果包含了则提取出响应对象,将输出流进行gzip压缩。毫无疑问,提取响应对象肯定应该在servlet被执行之后,但这个filter开始被执行的时机却是在servlet之前,它就依靠了递归调用的入栈跟出栈的执行机制。它同时有对HttpServletRequest以及HttpServletResponse对象的处理:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        logger.info("[doFilter] enter into CompressionFilter");
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;

        String encoding = Strings.nullToEmpty(req.getHeader("Accept-Encoding"));

        if (encoding.contains("gzip")) {
            CompressionResponseWrapper warppedResp = new CompressionResponseWrapper(resp);
            warppedResp.setHeader("Content-Encoding", "gzip");
            filterChain.doFilter(req, warppedResp);

            GZIPOutputStream gzos = warppedResp.getGZIPOutputStream();
            gzos.finish();
        } else {
            filterChain.doFilter(req, resp);
        }
    }

顺便提一下,只关注前进的话,只需要将递归调用放在最后一句即可(也就是让递归调用后面不再有代码逻辑)。

消息总线中遇到的问题

第一个问题:上面那篇文章中提到过,在接收消息的时候,由于RabbitMQ官方的java client,提供了一种阻塞等待的推送消费的API,这种模式对于client的用户不够友好,通常我们需要基于它构建一个独立的event loop(在另一个线程上),以onMessage的事件回调作为一种异步API的实现更为友好,因此它涉及到了chain中的处理器跨线程的问题,并且在event loop上会在chain尾部的几个处理器上作循环(消息等待与消息处理)。

第二个问题:web容器针对http区分开请求对象和响应对象,使得它们职责清晰,互不干扰,进退有度。而在消息总线中,单次处理消息的过程(只单独调用produce或consume)中不存在两个上下文对象的语义,这误导我只能把chain的进和退切开来,做成AOP的pre-aspect以及post-aspect(这种实现是我自上面提到的那篇文章之后的改进版)。

第三个问题:这是一大问题,我确实忽略了java实现filter“有进有退”的设计,而总是想着POSA那本书上有进无退的架构层面上的设计,这导致其中有个处理器是从对象池中借出对象,而归还的逻辑却不是在该处理器的递归调用点之后(而是在post-aspect中)。

改进方案

针对第一个问题:我换了个思维,如果我们将异步接收消息的整个chain都包含到event loop线程中(之前只是作为阻塞等待,并触发onMessage的一种实现机制),就解决了chain中有处理器需要跨线程问题;而将接收到消息之后的处理逻辑不再以处理器的方式拆分、复用,而是以继承以及子类化的方式进行复用,这解决了所谓的在chain“尾部循环”的问题。

针对第二、第三个问题:摈弃之前所谓的将一条chain切割成pre-aspect以及post-aspect的处理方式,采用了java中filter的设计方式,以递归调用的临界点的区分“进”、“退”逻辑。

写在最后

pipeline这个词在表述中一直不是太精确,有人用它表达责任链模式,有人将其代指pipeline&filter。在交流上自然不必太过较真,但我想从这篇文章可以给出一个很好的区分:责任链模式是设计模式,面向程序实现;pipeline&filter是架构模式。而不同点,我想你也看到了。

当时实现这部分代码的时候我正好在看POSA卷四,里面谈及了架构模式pipeline-filter。而我想这就是设计模式跟架构模式的区别:设计模式面向代码实现,而在代码实现中不关注运行代码的服务器的部署拓扑结构;而架构模式是一种抽象级别更高的模式,而且卷四本身就是面向分布式系统的,因此它所谓的filter其实是一个分布式的数据处理组件,这自然是一种纯粹的有进无退的的pipeline。

事实上这种设计到现在已经经历到第三版了。第一版是就是那篇文章中说明的方式,完全是POSA表达的那种;第二版,我有意看了一下java filter的实现,但当时被其两个上下文对象搞糊涂了,认为这是一种特定场景下的实现(有请求,有响应),并且我确实没有正视递归调用的实现起了主要作用。第三版,完全基于责任链的设计模式。

代码见: banyan

时间: 2024-08-07 14:37:11

再谈pipeline-filter模式的相关文章

再谈 X-UA-Compatible 兼容模式

如何理解 IE 的文档兼容模式(X-UA-Compatible)? IE 浏览器支持多种文档兼容模式,得以因此改变页面的渲染效果. IE9 模式支持全范围的既定行业标准,包括 HTML5(草案), W3C CSS Level 3 规范(草案), SVG 1.0 规范等 <meta http-equiv="X-UA-Compatible" content="IE=9"> IE8 模式支持许多既定行业标准,W3C CSS Level 2.1 规范和 W3C

再谈组合模式

http://acm.hdu.edu.cn/showproblem.php?pid=1507 大致题意:在一个n*m的格子上,黑色的地方不可用,问在白色格子上最多可放多少1*2的矩阵. 思路:建图,每个白色格子与它临近的上下左右的白色格子建边,求最大匹配,答案为最大匹配/2,因为是双向图.最后输出匹配边时,当找到一组匹配边记得将该边标记,以防重复计算. #include <stdio.h> #include <algorithm> #include <set> #inc

GoF设计模式三作者15年后再谈模式

Erich Gamma, Richard Helm, 和 Ralph Johnson在GoF设计模式发表15年以后,再谈模式,另外一位作者,也是四色原型的发明者Peter已经过世. 提问者:如今有85,000 iPhone的小应用遍布全球,使用PHP就能够写一个简单的"Hello, World! The time is X"Web网页,那么,面向对象设计是难的,这句话是否还正确呢? Richard Helm: 软件设计总是很难的,尽管大多数现代开发环境已经降低了复杂性,通过重用库和工具

WCF技术剖析之二:再谈IIS与ASP.NET管道

原文:WCF技术剖析之二:再谈IIS与ASP.NET管道 在2007年9月份,我曾经写了三篇详细介绍IIS架构和ASP.NET运行时管道的文章,深入介绍了IIS 5.x与IIS 6.0HTTP请求的监听与分发机制,以及ASP.NET运行时管道对HTTP请求的处理流程: [原创]ASP.NET Process Model之一:IIS 和 ASP.NET ISAPI [原创]ASP.NET Process Model之二:ASP.NET Http Runtime Pipeline - Part I

再谈ORACLE CPROCD进程

罗列一下有关oprocd的知识点 oprocd是oracle在rac中引入用来fencing io的 在unix系统下,如果我们没有采用oracle之外的第三方集群软件,才会存在oprocd进程 在linux系统下,只有在10.2.0.4版本后,才会具有oprocd进程 在window下,不会存在oprocd 进程,但是会存在一个oraFenceService服务,用来实现相同的功能,该服务采用的技术是基于windows的,与oprocd不同 oprocd进程可以运行在两者模式下:fatal和n

再谈multistage text input(中文输入法)下UITextView的内容长度限制

之前写过一篇<如何更好地限制一个UITextField的输入长度>,在文章最后得到的结论是可以直接使用 UIKIT_EXTERN NSString *const UITextFieldTextDidChangeNotification; 进行监听,截断超出maxLength的部分. 所以后来我在处理UITextView的内容长度时,也直接参考这个方法: [[NSNotificationCenter defaultCenter] addObserver:self selector:@select

从飞信群再谈时间管理

收邮件啊 快收邮件~取消飞信小群的当天晚上,便有几位小组组长跑到某某那如是说.虽然我没有做过调查,但是看到这样的情景,我想她们应该和我有一样的感觉,没有了飞信小群在一定程度上,不 -方 -便- 但是过了这一段时间之后,发现我们日常的学习并没有受到什么影响,反倒是比之前明显的改善了.下面谈谈我对这件事的看法. 首先我们还是有飞信大群的.因为不会经常通知,也就谈不上打扰,而且也保证了紧急情况下的及时性和效率.可小群不一样.小群的人数不多,但通知频繁.这样问题就随之来了.最近我的体会 优点一减少打扰

再谈word2003编程

再谈word2003编程 近期因为项目需要,写了许多word2003编程的东东.有时候遇到难题想查sdk说明,很难找到中文解释,对于e文不好的我来说,简直是天书.想必很多人多有感慨.     下面列出内容是一些常用的内容说明,希望对大家有帮助.     那就开始吧注意,下文的WAPP是我定义的word文档工程变量 的 //合并单元格    table.Cell(2, 2).Merge(table.Cell(2, 3));  //单元格分离     object Rownum = 2;     o

Unity教程之再谈Unity中的优化技术

这是从 Unity教程之再谈Unity中的优化技术 这篇文章里提取出来的一部分,这篇文章让我学到了挺多可能我应该知道却还没知道的知识,写的挺好的 优化几何体 这一步主要是为了针对性能瓶颈中的”顶点处理“一项.这里的几何体就是指组成场景中对象的网格结构. 3D游戏制作都由模型制作开始.而在建模时,有一条我们需要记住:尽可能减少模型中三角形的数目,一些对于模型没有影响.或是肉眼非常难察觉到区别的顶点都要尽可能去掉.例如在下面左图中,正方体内部很多顶点都是不需要的,而把这个模型导入到Unity里就会是