Intercepting Filter模式详解

问题描述

在服务器编程中,通常需要处理多种不同的请求,在正式处理请求之前,需要对请求做一些预处理,如:

  1. 纪录每个Client的每次访问信息。
  2. 对Client进行认证和授权检查(Authentication and Authorization)。
  3. 检查当前Session是否合法。
  4. 检查Client的IP地址是否可信赖或不可信赖(IP地址白名单、黑名单)。
  5. 请求数据是否先要解压或解码。
  6. 是否支持Client请求的类型、Browser版本等。
  7. 添加性能监控信息。
  8. 添加调试信息。
  9. 保证所有异常都被正确捕获到,对未预料到的异常做通用处理,防止给Client看到内部堆栈信息。

在响应返回给客户端之前,有时候也需要做一些预处理再返回:

  1. 对响应消息编码或压缩。
  2. 为所有响应添加公共头、尾等消息。
  3. 进一步Enrich响应消息,如添加公共字段、Session信息、Cookie信息,甚至完全改变响应消息等。

如何实现这样的需求,同时保持可扩展性、可重用性、可配置、移植性?

问题解决

要实现这种需求,最直观的方法就是在每个请求处理过程中添加所有这些逻辑,为了减少代码重复,可以将所有这些检查提取成方法,这样在每个处理方法中调用即可:

public Response service1(Request request) {
    validate(request);
    request = transform(request);
    Response response = process1(request);
    return transform(response);
}

此时,如果出现service2方法,依然需要拷贝service1中的实现,然后将process1换成process2即可。这个时候我们发现很多重复代码,继续对它重构,比如提取公共逻辑到基类成模版方法,这种使用继承的方式会引起子类对父类的耦合,如果要让某些模块变的可配置需要有太多的判断逻辑,代码变的臃肿;因而可以更进一步,将所有处理逻辑抽象出一个Processor接口,然后使用Decorate模式(即引用优于继承):

public interface Processor {
    Response process(Request request);
}
public class CoreProcessor implements Processor {
    public Response process(Request request) {
        // do process/calculation
    }
}
public class DecoratedProcessor implements Processor {
    private final Processor innerProcessor;
    public DecoratedProcessor(Processor processor) {
        this.innerProcessor = processor;
    }

    public Response process(Request request) {
        request = preProcess(request);
        Response response = innerProcessor.process(request);
        response = postProcess(response);
        return response;
    }

    protected Request preProcess(Request request) {
        return request;
    }
    protected Response postProcess(Response response) {
        return response;
    }
}

public void Transformer extends DecoratedProcessor {
    public Transformer(Processor processor) {
        super(processor);
    }

    protected Request preProcess(Request request) {
        return transformRequest(request);
    }
    protected Response postProcess(Response response) {
        return transformResponse(response);
    }
}

此时,如果需要在真正的处理逻辑之前加入其他的预处理逻辑,只需要继承DecoratedProcessor,实现preProcess或postProcess方法,分别在请求处理之前和请求处理之后横向切入一些逻辑,也就是所谓的AOP编程:面向切面的编程,然后只需要根据需求构建这个链条:

Processor processor = new MissingExceptionCatcher(new Debugger(new Transformer(new CoreProcessor());
Response response = processor.process(request);
......

这已经是相对比较好的设计了,每个Processor只需要关注自己的实现逻辑即可,代码变的简洁;并且每个Processor各自独立,可重用性好,测试方便;整条链上能实现的功能只是取决于链的构造,因而只需要有一种方法配置链的构造即可,可配置性也变得灵活;然而很多时候引用是一种静态的依赖,而无法满足动态的需求。要构造这条链,每个前置Processor需要知道其后的Processor,这在某些情况下并不是在起初就知道的。此时,我们需要引入Intercepting Filter模式来实现动态的改变条链。

Intercepting Filter模式

在前文已经构建了一条由引用而成的Processor链,然而这是一条静态链,并且需要一开始就能构造出这条链,为了解决这个限制,我们可以引入一个ProcessorChain来维护这条链,并且这条链可以动态的构建。

有多种方式可以实现并控制这个链:

  1. 在存储上,可以使用数组来存储所有的Processor,Processor在数组中的位置表示这个Processor在链条中的位置;也可以用链表来存储所有的Processor,此时Processor在这个链表中的位置即是在链中的位置。
  2. 在抽象上,可以所有的逻辑都封装在Processor中,也可以将核心逻辑使用Processor抽象,而外围逻辑使用Filter抽象。
  3. 在流程控制上,一般通过在Processor实现方法中直接使用ProcessorChain实例(通过参数掺入)来控制流程,利用方法调用的进栈出栈的特性实现preProcess()和postProcess()处理。

在实际中使用这个模式的有:Servlet的Filter机制、Netty的ChannelPipeline中、Structs2中的Interceptor中都实现了这个模式。

Intercepting Filter模式在Servlet的Filter中的实现(Jetty版本)

其中Servlet的Filter在Jetty的实现中使用数组存储Filter,Filter末尾可以使用Servlet实例处理真正的业务逻辑,在流程控制上,使用FilterChain的doFilter方法来实现。如FilterChain在Jetty中的实现:

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException 
    // pass to next filter
    if (_filter < LazyList.size(_chain)) {
        FilterHolder holder= (FilterHolder)LazyList.get(_chain, _filter++);
        Filter filter= holder.getFilter();
        filter.doFilter(request, response, this);                    
        return;
    }

    // Call servlet
    HttpServletRequest srequest = (HttpServletRequest)request;
    if (_servletHolder != null) {
        _servletHolder.handle(_baseRequest,request, response);
    }
}

这里,_chain实际上是一个Filter的ArrayList,由FilterChain调用doFilter()启动调用第一个Filter的doFilter()方法,在实际的Filter实现中,需要手动的调用FilterChain.doFilter()方法来启动下一个Filter的调用,利用方法调用的进栈出栈的特性实现Request的pre-process和Response的post-process处理。如果不调用FilterChain.doFilter()方法,则表示不需要调用之后的Filter,流程从当前Filter返回,在它之前的Filter的FilterChain.doFilter()调用之后的逻辑反向处理直到第一个Filter处理完成而返回。

public class MyFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // pre-process ServletRequest
        chain.doFilter(request, response);
        // post-process Servlet Response
    }
}

整个Filter链的处理流程如下:

Intercepting Filter模式在Netty3中的实现

Netty3在DefaultChannelPipeline中实现了Intercepting Filter模式,其中ChannelHandler是它的Filter。在Netty3的DefaultChannelPipeline中,使用一个以ChannelHandlerContext为节点的双向链表来存储ChannelHandler,所有的横切面逻辑和实际业务逻辑都用ChannelHandler表达,在控制流程上使用ChannelHandlerContext的sendDownstream()和sendUpstream()方法来控制流程。不同于Servlet的Filter,ChannelHandler有两个子接口:ChannelUpstreamHandler和ChannelDownstreamHandler分别用来请求进入时的处理流程和响应出去时的处理流程。对于Client的请求,从DefaultChannelPipeline的sendUpstream()方法入口:

public void sendDownstream(ChannelEvent e) {
    DefaultChannelHandlerContext tail = getActualDownstreamContext(this.tail);
    if (tail == null) {
        try {
            getSink().eventSunk(this, e);
            return;
        } catch (Throwable t) {
            notifyHandlerException(e, t);
            return;
        }
    }
    sendDownstream(tail, e);
}
void sendDownstream(DefaultChannelHandlerContext ctx, ChannelEvent e) {
    if (e instanceof UpstreamMessageEvent) {
        throw new IllegalArgumentException("cannot send an upstream event to downstream");
    }
    try {
        ((ChannelDownstreamHandler) ctx.getHandler()).handleDownstream(ctx, e)
     } catch (Throwable t) {
        e.getFuture().setFailure(t);
        notifyHandlerException(e, t);
    }
}

如果有响应消息,该消息从DefaultChannelPipeline的sendDownstream()方法为入口:

public void sendUpstream(ChannelEvent e) {
    DefaultChannelHandlerContext head = getActualUpstreamContext(this.head);
    if (head == null) {
        return;
    }
    sendUpstream(head, e);
}
void sendUpstream(DefaultChannelHandlerContext ctx, ChannelEvent e) {
    try {
        ((ChannelUpstreamHandler) ctx.getHandler()).handleUpstream(ctx, e);
    } catch (Throwable t) {
        notifyHandlerException(e, t);
    }
}

在实际实现ChannelUpstreamHandler或ChannelDownstreamHandler时,调用ChannelHandlerContext中的sendUpstream或sendDownstream方法将控制流程交给下一个ChannelUpstreamHandler或下一个ChannelDownstreamHandler,或调用Channel中的write方法发送响应消息。

public class MyChannelUpstreamHandler implements ChannelUpstreamHandler {
    public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
        // handle current logic, use Channel to write response if needed.
        // ctx.getChannel().write(message);
        ctx.sendUpstream(e);
    }
}

public class MyChannelDownstreamHandler implements ChannelDownstreamHandler {
    public void handleDownstream(
            ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
        // handle current logic
        ctx.sendDownstream(e);
    }
}

当ChannelHandler向ChannelPipelineContext发送事件时,其内部从当前ChannelPipelineContext 节点出发找到下一个ChannelUpstreamHandler或ChannelDownstreamHandler实例,并向其发送 ChannelEvent,对于Downstream链,如果到达链尾,则将ChannelEvent发送给ChannelSink:

public void sendDownstream(ChannelEvent e) {
    DefaultChannelHandlerContext prev = getActualDownstreamContext(this.prev);
    if (prev == null) {
        try {
            getSink().eventSunk(DefaultChannelPipeline.this, e);
        } catch (Throwable t) {
            notifyHandlerException(e, t);
        }
    } else {
        DefaultChannelPipeline.this.sendDownstream(prev, e);
    }
}

public void sendUpstream(ChannelEvent e) {
    DefaultChannelHandlerContext next = getActualUpstreamContext(this.next);
    if (next != null) {
        DefaultChannelPipeline.this.sendUpstream(next, e);
    }
}

正是因为这个实现,如果在一个末尾的ChannelUpstreamHandler中先移除自己,在向末尾添加一个新的ChannelUpstreamHandler,它是无效的,因为它的next已经在调用前就固定设置为null了。

在DefaultChannelPipeline的ChannelHandler链条的处理流程为:

在这个实现中,不像Servlet的Filter实现利用方法调用栈的进出栈来完成pre-process和post-process,而是在进去的链和出来的链各自调用handleUpstream()和handleDownstream()方法,这样会引起调用栈其实是两条链的总和,因而需要注意这条链的总长度。这样做的好处是这条ChannelHandler的链不依赖于方法调用栈,而是在DefaultChannelPipeline内部本身的链,因而在handleUpstream()或handleDownstream()可以随时将执行流程转发给其他线程或线程池,只需要保留ChannelPipelineContext引用,在处理完成后用这个ChannelPipelineContext重新向这条链的后一个节点发送ChannelEvent,然而由于Servlet的Filter依赖于方法的调用栈,因而方法返回意味着所有执行完成,这种限制在异步编程中会引起问题,因而Servlet在3.0后引入了Async的支持。

Intercepting Filter模式的缺点

简单提一下这个模式的缺点:
1. 相对传统的编程模型,这个模式有一定的学习曲线,需要很好的理解该模式后才能灵活的应用它来编程。
2. 需要划分不同的逻辑到不同的Filter中,这有些时候并不是那么容易。
3. 各个Filter之间共享数据将变得困难。在Netty3中可以自定义自己的ChannelEvent来实现自定义消息的传输,或者使用ChannelPipelineContext的Attachment字段来实现消息传输,而Servlet中的Filter则没有提供类似的机制,如果不是可以配置的数据在Config中传递,其他时候的数据共享需要其他机制配合完成。

时间: 2025-01-27 16:03:27

Intercepting Filter模式详解的相关文章

PHP的XML模式详解

研究与 php(做为现在的主流开发语言) 5 捆绑在一起的 xml(标准化越来越近了)Reader 库,它使 php(做为现在的主流开发语言) 页面能够以高效的流模式来处理 xml(标准化越来越近了) 文档. php(做为现在的主流开发语言) 5 引入了新的类 xml(标准化越来越近了)Reader,用于读取可扩展标记语言(Extensible Markup Language,xml(标准化越来越近了)).与 Simplexml(标准化越来越近了) 或文档对象模型(Document Object

CSS盒子模式详解二

自本人教程<CSS盒子模式详解>发表以来,一直获得各位网友的好评与支持,今天应各位网友的要求,也为了答谢各位网友的支持,本人将推出续篇教程,以加深各位网友对CSS盒子模式的理解.此教程面向读者还是DIV排版入门者,如果你是高手,那就不要浪费自己时间了. 本人这次将讲解一个纵向CSS导航栏实例的制作,在讲解过程中将会延用之前发表教程所讲到的"盒子"概念,如果你没有看过本人之前的那篇教程,又或者你还不理解什么是"盒子"这一概念,请先通过下面的两个链接阅读本人

代理模式(proxy pattern) 未使用代理模式 详解

如果需要监控(monitor)类的某些状态, 则需要编写一个监控类, 并同过监控类进行监控. 但仅仅局限于本地, 如果需要远程监控, 则需要使用代理模式(proxy pattern). 具体方法: 1. 类中需要提供状态信息, 并提供一些get方法, 进行调用. /** * @time 2014年7月11日 */ package proxy; /** * @author C.L.Wang * */ public class GumballMachine { String location; //

Javascript的严格模式详解

一.概述 除了正常运行模式,ECMAscript 5添加了第二种运行模式:"严格模式"(strict mode).顾名思义,这种模式使得Javascript在更严格的条件下运行. 设立"严格模式"的目的,主要有以下几个: - 消除Javascript语法的一些不合理.不严谨之处,减少一些怪异行为; - 消除代码运行的一些不安全之处,保证代码运行的安全: - 提高编译器效率,增加运行速度: - 为未来新版本的Javascript做好铺垫. "严格模式&quo

深入理解JavaScript系列(26):设计模式之构造函数模式详解

 这篇文章主要介绍了深入理解JavaScript系列(26):设计模式之构造函数模式详解,本文讲解了基本用法.构造函数与原型.只能用new吗?.强制使用new.原始包装函数等内容,需要的朋友可以参考下     介绍 构造函数大家都很熟悉了,不过如果你是新手,还是有必要来了解一下什么叫构造函数的.构造函数用于创建特定类型的对象--不仅声明了使用的对象,构造函数还可以接受参数以便第一次创建对象的时候设置对象的成员值.你可以自定义自己的构造函数,然后在里面声明自定义类型对象的属性或方法. 基本用法 在

深入理解JavaScript系列(27):设计模式之建造者模式详解

 这篇文章主要介绍了深入理解JavaScript系列(27):设计模式之建造者模式详解,建造者模式可以将一个复杂对象的构建与其表示相分离,使得同样的构建过程可以创建不同的表示,需要的朋友可以参考下     介绍 在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成:由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定.如何应对这种变化?如何提供一种"封装机制"来隔离出"复

深入理解JavaScript系列(28):设计模式之工厂模式详解

 这篇文章主要介绍了深入理解JavaScript系列(28):设计模式之工厂模式详解,工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类,需要的朋友可以参考下     介绍 与创建型模式类似,工厂模式创建对象(视为工厂里的产品)时无需指定创建对象的具体类. 工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类.该模式使一个类的实例化延迟到了子类.而子类可以重写接口方法以便创建的时候指定自己的对象类型. 这个模式十分有用,尤其是创建对象的流程赋值的时候,比如依赖于

深入理解JavaScript系列(29):设计模式之装饰者模式详解

 这篇文章主要介绍了深入理解JavaScript系列(29):设计模式之装饰者模式详解,装饰者用用于包装同接口的对象,不仅允许你向方法添加行为,而且还可以将方法设置成原始对象调用(例如装饰者的构造函数),需要的朋友可以参考下     介绍 装饰者提供比继承更有弹性的替代方案. 装饰者用用于包装同接口的对象,不仅允许你向方法添加行为,而且还可以将方法设置成原始对象调用(例如装饰者的构造函数). 装饰者用于通过重载方法的形式添加新功能,该模式可以在被装饰者前面或者后面加上自己的行为以达到特定的目的.

深入理解JavaScript系列(30):设计模式之外观模式详解

 这篇文章主要介绍了深入理解JavaScript系列(30):设计模式之外观模式详解,外观模式(Facade)为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口值得这一子系统更加容易使用,需要的朋友可以参考下     介绍 外观模式(Facade)为子系统中的一组接口提供了一个一致的界面,此模块定义了一个高层接口,这个接口值得这一子系统更加容易使用. 正文 外观模式不仅简化类中的接口,而且对接口与调用者也进行了解耦.外观模式经常被认为开发者必备,它可以将一些复杂操作封装