设计模式责任链模式
行为型模式
责任链模式 将多个对象连成一个链条,并沿着这条链传递请求,使得每个对象都有机会处理请求,直到有一个对象或者每个对象都能处理这个请求。 组成
多个对象要能组成一个"链表"结构,每个对象应该继承自同一个类或实现同一个接口,并且每个对象要持有下一个对象的引用。因此,责任链的组成如下图所示:
图1. 责任链模式的组成
图中各个组成的作用: Handler:定义一个处理请求的接口,持有对自身的引用; ConcreteHandler:接口的具体实现,负责处理请求或转发请求,需要能判断"是否处理当前请求";
从链中第一个对象开始,每个对象要么处理请求,要么转发给下一个对象。提交请求的对象(用户、客户端)并不知道哪一个对象会处理他的请求,只能保证他的请求一定会被处理(请求有一个 隐式的接收者 )。请求要能够沿着链条转发,并且保证接收者是隐式的,链上每个对象都要有一致的处理请求和转发给后一个对象的接口(有相同的对外方法)。 应用场景
当存在以下情况时,使用责任链 模式: 有多个对象可以处理一个请求,哪个对象处理该请求在运行时自动确定; 想在不明确指定接受者的情况下,向多个对象中的一个提交一个请求; 可处理一个请求的对象集合 应该能够被动态指定 ; 示例代码 // Handler 接口 public interface Handler { // 对后继者的引用 protected Handler next; /** * 处理方法 */ public void handle(Request request); } // 实现类A public class HandlerA implements Handler { @Override public void handle(Request request) { // TODO 实现自己的处理逻辑 // .... // 指派给下一个 if(next != null) { next.handle(request); } } } // 实现类B public class HandlerB implements Handler { @Override public void handle(Request request) { // TODO 实现自己的处理逻辑 // .... // 指派给下一个 if(next != null) { next.handle(request); } } } // 把 HandlerA 和 HandlerB 组成一个链条 HandlerA handlerA = new HandlerA(); HandlerB handlerB = new HandlerB(); handlerA.setNext(handlerB);过滤器
"责任链模式"的一个经典示例是容器的过滤器。 FilterChain
因为责任链中每个对象要能指向下一个对象,所以责任链的一个关键问题是:如何维护实现类之间的连接关系。上面的示例代码中,使用指向下一个对象的引用字段 next 来维护这种关系,过滤器使用一个对象 javax.servlet.FilterChain 来维护一个对象数组。 FilterChain 接口的 UML 如下图所示:
图2. FilterChain 接口的实现关系
FilterChain 代码如下: public interface FilterChain { public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException; }
FilterChain 只提供了一个方法,请求参数是 "request、response",从前面对"责任链"的介绍可知,在该方法中既要要调用链中对象的过滤方法,还要能请求参数转发到链中的下一个对象。下面,通过它的实现类 ApplicationFilterChain 查看它的实现。 ApplicationFilterChain public final class ApplicationFilterChain implements FilterChain { /** * Filters 数组 */ private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; /** * Filters 数组处理请求的对象下标 */ private int pos = 0; /** * Filters 数组中对象的个数 */ private int n = 0; // FilterChain Methods @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // ...其他的省略,主要是调用了这个方法执行过滤.... internalDoFilter(req,res); } // 真正处理请求的方法 private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // 如果当前下标 pos < n,就取出当前对象,执行对象的 doFilter() 方法 // 注意:pos++ if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if( Globals.IS_SECURITY_ENABLED ) { // 省略... } else { // 执行过滤器方法 filter.doFilter(request, response, this); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.filter"), e); } return; } } /** * 把一个过滤器添加到 filters 数组中 */ void addFilter(ApplicationFilterConfig filterConfig) { // Prevent the same filter being added multiple times for(ApplicationFilterConfig filter:filters) if(filter==filterConfig) return; if (n == filters.length) { ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig[n + INCREMENT]; System.arraycopy(filters, 0, newFilters, 0, n); filters = newFilters; } filters[n++] = filterConfig; } }
从源码得到以下信息: 有一个 ApplicationFilterConfig 数组(对象数组),每个 ApplicationFilterConfig 对象持有一个 Filter 变量; 参数 pos 、 n 分别表示当前数组中正在使用的过滤器下标和数组的总长度,每执行一次方法,pos 值就会加1; 在启动时, addFilter() 方法把 ApplicationFilterConfig 对象添加到数组中,并且保证了不会重复添加; 在 internalDoFilter() 方法中,取出数组中 pos 下标处的 filterConfig 和 filter ,然后执行 filter 对象 doFilter() 方法;
在上面的代码中,并没有看到当前对象把请求转发给下一个对象(执行了 pos++,把数组下标加1),对象只执行了自身的 doFileter() 方法,很显然转发操作就发生在这个 Filter 对象的 doFileter() 方法中。 Filter
Filter 接口的源码如下: public interface Filter { public default void init(FilterConfig filterConfig) throws ServletException {} // 除了 FilterChain 中的两个参数,还传入了 FilterChain 对象 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; public default void destroy() {} }
doFilter() 方法中传入了3个参数:request、response、FilterChain。
进入到最简单的 Filter 实现类 SessionInitializerFilter 中,该方法代码如下: public class SessionInitializerFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ((HttpServletRequest)request).getSession(); chain.doFilter(request, response); } }
在 doFilter() 方法中,调用了 FilterChain.doFilter() 方法。从前面已知 FilterChain.doFilter() 方法会取出下标 pos 处的 Filter,而在调用当前 Filter 对象的 doFilter() 方法之前,应执行了 pos++ 方法,把 pos 值加1,所以这里会取出数组中下一个 Filter,从而实现了请求转发。
因此,每个 Filter 实现类在处理完自己的逻辑后,通过调用 FilterChain 的方法来把请求转发给下一个过滤器。整个过程和时序图如下图所示:
图3.1 Filter的调用和转发过程
图3.2. Filter的方法调用时序图
前面展示了 指针、数组 形式的"责任链"实现方式,实际上"责任链"的实现方式是很多样的,需要根据业务需求选择一个合适的方式来实现,比如 Netty 的 ChannelPipeline 使用了一个双向链表实现了 ChannelHandler 的责任链。 优点和缺点请求的提交者(用户、客户端)并不知道最终是由哪个对象处理他的请求,只知道他的请求会被处理,这降低了请求与处理对象之间的耦合; 我们可以根据业务需要,动态地增加、删减责任链上的处理对象,而不会影响到已有的代码,增强了指派职责的灵活; 总结
对同一个请求要连续做多种不同的处理时,可以使用责任链模式。
和"策略模式"很相似,都能实现"请求与处理对象解耦",但"责任链模式"可以依次执行多个方法,"策略模式"是在多个方法中选择一个。 参考阅读
1、Erich Gamma, Richard Helm, Ralph Johnson, Jojn Vlissides. 设计模式--可复用面向对象软件的基础[M]. 机械工业出版社,北京. 2018.11
国际足协女足世界杯已经卖出超过50万张门票,中国是重点购买者在这个阖家欢乐的时刻,首先祝所有的读者和球迷朋友们新年快乐,兔年大吉!精彩的卡塔尔世界杯转瞬即逝,短短一个月的时间却让许多球迷难以忘怀。而在今年的七月份,我们将会迎来女足世界杯。本
这是脐疝吗?需要怎么护理呀?新生儿由于腹压比较高,在哭闹或者肚子胀气时,会引发脐疝。脐疝是指腹腔内容物由脐部薄弱区突出的腹外疝。脐位于腹壁正中部,在胚胎发育过程中,是腹壁最晚闭合的部位。脐部缺少脂肪组织,使腹
更年期女性应该如何避孕?点击蓝字关注我们更年期女性应该如何避孕?更年期的女性卵巢功能开始下降,月经变得不规律,经量也明显稀少,但仍会有不规则的排卵。只要有成熟的卵泡排出,女性就有怀孕的可能。据世界卫生组织
我在这里过大年五百岁的福山大集将成打卡地大小新闻1月20日讯(YMG全媒体记者邹春霞通讯员刘丽丽摄影报道)在福山大集里转一圈,身上一定沾满了年味儿。春节前夕,在中国许多地方都有赶大集备年货的传统。福山大集作为烟台古老的集
812万盆鲜花装点最美回家路昆明欢迎你对于在外工作的云南人来说,回乡的第一站通常落地昆明。近日,昆明用鲜花常开不败的面貌迎接着归乡的云南人。1月19日,春城晚报开屏新闻记者从昆明市园林绿化局了解到,今年昆明市在春节氛围
多彩石家庄非遗过大年丨年味儿里的石家庄传统美食过大年1月14日,由石家庄市文化广电和旅游局主办,河北新闻网承办,各县(市区)文化和旅游部门协办的玉兔迎春多彩石家庄非遗过大年系列活动正式开启!活动将陆续推出年味儿里的石家庄非遗展览,介
创名县看今昭过年不打烊,嗨尽兴,就在昭化古城!新年倒计时打铁花1月22日1月27日(大年初一至初六)每晚945分网络图初一至初六连续六天在昭化古城太极广场上一起欣赏漫天星光场面震撼的传统民俗技艺打铁花!在绚烂的火花下许下对未来
兔然来袭!福山区一大波文旅活动拉满过年氛围!倒计时春节假期即将到来激动的心颤抖的手小编已经迫不及待啦你是不是也做好过年的准备啦实不相瞒福山区文化和旅游局新春套餐已经准备好了初一到十五精彩不重样,兔您乐呵!2023NEWYEA
重庆喊你来过年共享冰雪奇缘逍遥巫溪请你吃烤鱼岁末临近,新春将至你去哪里过年?快来巫溪吃地道的烤鱼耍刺激滑雪探古老盐文化这个春节逍遥巫溪邀你,同享冰雪奇缘!红池坝国家森林公园红池坝国家森林公园,海拔在1100米至2630米之间
春节出游十大热门目的地之一的长沙给您准备了这份文旅盛宴菜单焰火点亮星城。除署名外均为资料图片炭河古城过大年活动。华谊兄弟(长沙)电影小镇一路好戏过大年主题活动将欢乐亮相。迎新春原创大展喜湘逢湖南吉祥艺术大观。长沙晚报全媒体记者邹麟摄长沙晚
绵竹这个景区将开展游园活动,还有新年礼品拿哦兔年大吉新年伊始万象更新,新年的曙光已经扑面而来!为营造新年欢乐的氛围,丰富和活跃节日期间群众文化生活,让游客通过参与活动的形式感受到新年年味儿,绵竹市汉旺地震遗址公园特举办喜迎新