logo头像

学如逆水行舟,不进则退!!!

文章目录

责任链设计及各种变体,彻底搞懂责任链

设计模式是对代码的一种规范,并不局限于语言。他的主题思想主要是:单一职责原则开放封闭原则里氏替换原则依赖倒置原则迪米特原则接口隔离原则

责任链概念图

  • 其中按照功能分为三大功能: 创建型模式、结构型模式、行为型模式。而我们今天的主角就是属于行为型模式的责任链模式。

具体场景

  • 还记得上次离职是什么时候吗?离职的流程麻烦吗?各种的手续各种的审批搞的自己是焦头烂额。
  • 试想想为什么他们不能自动扭转呢?为什么我不能提交申请,剩下他们自己内部开始扭转?今天我就要用责任链模式来实现一个离职申请流程。

UML 结构

  • 首先,我对原始的责任链做了一个简单的修改。个人喜欢面向接口开发。所以我这里多了一个 StandoutHandler 接口。这个接口里规定了存在哪些方法。

StandoutHandler

  • 我们只需要暴露出来对下一个节点的设置和获取。除此之外我们只需要再暴露出来一个执行方法即可。
public interface StandoutHandler {
    /**
     * 获取责任链上下一个执行器
     * @return
     */
    public StandoutHandler getNextHandler();
    public void setNextHandler(StandoutHandler handler);

    public void execute(Request request);
}

AbstractHandler

  • 在 execute 方法中算是父类方法中,为什么这里我还要在抽离出一个 run 方法呢?只有这样 execute 才能起到通用的作用,至于 run 交给每个子类实现,这样方便我们对通用型的逻辑进行处理,比如现在我想对每个子类的动作进行记录,那么我可以在 execute 方法中进行拦截,这里的 execute 起到一个 AOP 的作用。
  • 我们的 execute 方法逻辑也很简单,先执行自己的业务,执行结束后放行到下一节点。
public abstract class AbstractHandler implements StandoutHandler{
    private StandoutHandler nextHandler;

    @Override
    public StandoutHandler getNextHandler() {
        return nextHandler;
    }

    @Override
    public void setNextHandler(StandoutHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public abstract void run(Request request);

    @Override
    public void execute(Request request) {
        run(request);
        if (this.nextHandler != null) {
            this.nextHandler.execute(request);
        }
    }
}

DefaultConcrete

  • 为了避免无法预料的异常情况,所以这里我内置了一个具体实现类放置在责任链的顶端。
public class DefaultConcreteHandler extends AbstractHandler {
    @Override
    public void run(Request request) {
        System.out.println(String.format("我是%s审批...接下来我将自动交给%s处理",this.getClass().getSimpleName(),getNextHandler().getClass().getSimpleName()));
    }
}
  • 这里为了演示效果,我在每个具体实现类中都是简单的打印内容。你可能会说为什么不把他放到 execute 方法上?我这里纯粹是为了掩饰在 run 中的业务操作。

  • 所以其他 4 个具体实现类,我这里就不贴代码了,读者自己可以修改类名即可。

Client

  • 接口、抽象、具体实现都已经准备好了,剩下的我们还缺一个对责任链的组装功能。这里 1 个 Client 我们可以组装出一个链条,从而对应我们一套业务。
public class Client {
    private static Client client = new Client();
    private StandoutHandler standoutHandler = new DefaultConcreteHandler();
    private StandoutHandler lastStandoutHandler = standoutHandler;

    public static Client getInstance() {
        return client;
    }
    public StandoutHandler registerHandler(StandoutHandler handler) {
        this.lastStandoutHandler.setNextHandler(handler);
        this.lastStandoutHandler = handler;
        return handler;
    }
    public StandoutHandler build() {
        return standoutHandler;
    }
}
  • 这里我简单的使用了数据结构中链表的思想来组装出这个链条。有了链条下面我们就可以直接使用了。

Test

public class Test {
    public static void main(String[] args) {
        //init chains
        Client.getInstance().registerHandler(new ConcreteHandler1());
        Client.getInstance().registerHandler(new ConcreteHandler2());
        Client.getInstance().registerHandler(new ConcreteHandler3());
        Client.getInstance().registerHandler(new ConcreteHandler4());
        StandoutHandler standoutHandler = Client.getInstance().build();
        standoutHandler.execute(new Request());
    }
}

效果

  • 接下来我们看看上面的测试出现的效果吧。

  • 这里我们可以看到,在请求发起者(Test) 只需要通过 Client 获取到责任链并将请求数据传递给它,剩下的就无需关注了,内部的流程完全是责任链自己处理。我们可以清楚的看到流程从 DefaultConcrete -> ConcreteHandler1 -> ConcreteHandler2 -> ConcreteHandler3 -> ConcreteHandler4

  • 在子类(run 方法)的业务逻辑中完全是独立的,他们之间完全隔离唯一共同点就是他们拥有同个参数传递对象。

优势在哪

  • 上面我们从理论到实践介绍了责任链模式。那么他的优势在哪里?为什么要使用责任链?

  • 责任链模式下请求的发起者而言可以说是: 衣来伸手、饭来张口。发起者完全充当一个透明人的角色,内部的逻辑不需要了解,也没必要了解,最终只要一个结果。

责任链变形之双向链

  • 我们一直都知道源码中处处都是设计模式的体现,每当我熟悉设计模式之后准备去源码里挑战一下,结果一看发现跟我了解到的设计模式不太一样,看着看着就把自己绕进去了。
  • 其实这并不是说明源码中不遵从设计模式,而是在设计模式的基础上会做适当的变换。就比如我们针对上面的责任链模式稍作变形就符合了数据库连接这个场景了。

幻想需求

  • 我们都知道数据库连接涉及到连接和释放两个动作。此时我有这样一个需求,上面提到的 4 个具体实现类上分别需要创建不同的数据库连接进行操作,每次操作完都需要对资源进行释放,我这里还是那句话,完全是为了模拟出场景从而体现设计模式的价值所在。

  • 通过上面的图示我们也能感觉出来,责任链感觉由单链表变成的双链表了。我们可以将连接和释放抽象成两个方法,链表内部通过两个指针进行双向绑定,这样就能从头至尾、从尾至头执行,话不多说我们开始上代码。

StandoutHandler

public interface StandoutHandler {
    /**
     * 获取责任链上下一个执行器
     * @return
     */
    public StandoutHandler getNextHandler();
    public void setNextHandler(StandoutHandler handler);

    /**
     * 获取责任链上下一个执行器
     * @return
     */
    public StandoutHandler getPreHandler();
    public void setPreHandler(StandoutHandler handler);

    public void connect(Request request);
    public void close(Request request);
}

AbstractHandler

public abstract class AbstractHandler implements StandoutHandler{
    private StandoutHandler nextHandler;
    private StandoutHandler preHandler;

    @Override
    public StandoutHandler getNextHandler() {
        return nextHandler;
    }

    @Override
    public void setNextHandler(StandoutHandler nextHandler) {
        this.nextHandler = nextHandler;
    }
    @Override
    public StandoutHandler getPreHandler() {
        return preHandler;
    }

    @Override
    public void setPreHandler(StandoutHandler nextHandler) {
        this.preHandler = nextHandler;
    }

    public abstract void runConnect(Request request);
    public abstract void runClose(Request request);

    @Override
    public void connect(Request request) {
        runConnect(request);
        if (this.nextHandler != null) {
            this.nextHandler.connect(request);
        }
    }
    @Override
    public void close(Request request) {
        runClose(request);
        if (this.preHandler != null) {
            this.preHandler.close(request);
        }
    }
}

DefaultConcrete

public class DefaultConcreteHandler extends AbstractHandler {
    @Override
    public void runConnect(Request request) {
        System.out.println(String.format("我是%s正在开启连接...接下来我将自动交给%s处理",this.getClass().getSimpleName(),getNextHandler().getClass().getSimpleName()));
    }
    @Override
    public void runClose(Request request) {
        System.out.println(String.format("我是%s正在关闭连接...接下来我将自动交给%s处理",this.getClass().getSimpleName(),getNextHandler().getClass().getSimpleName()));
    }
}

Client

public class Client {
    private static Client client = new Client();
    private StandoutHandler standoutHandler = new DefaultConcreteHandler();
    private StandoutHandler lastStandoutHandler = standoutHandler;

    public static Client getInstance() {
        return client;
    }
    public StandoutHandler registerHandler(StandoutHandler handler) {
        handler.setPreHandler(this.lastStandoutHandler);
        this.lastStandoutHandler.setNextHandler(handler);
        this.lastStandoutHandler = handler;
        return handler;
    }
    public StandoutHandler build() {
        return standoutHandler;
    }
    public StandoutHandler hail() {
        return lastStandoutHandler;
    }
}

Test

public class Test {
    public static void main(String[] args) {
        //init chains
        Client.getInstance().registerHandler(new ConcreteHandler1());
        Client.getInstance().registerHandler(new ConcreteHandler2());
        Client.getInstance().registerHandler(new ConcreteHandler3());
        Client.getInstance().registerHandler(new ConcreteHandler4());
        StandoutHandler standoutHandler = Client.getInstance().build();
        standoutHandler.connect(new Request());
        StandoutHandler hail = Client.getInstance().hail();
        hail.close(new Request());
    }
}

效果

  • 这样我们通过双向链表实现了如何控制责任链的方向

责任链变形之环形链

  • 环形的责任链使用场景是啥呢?这点主要是方便 Client 组装而言的,试想一下我们现在想做一个促销打折的功能,每个实现类对应一种打折活动,且内部我们的打折是不会出现重叠的情况的,比如 ConcreteHandler1 对应消费金额介于 100~200 则减 10,ConcreteHandler2 在消费金额介于 300-350 则减 15 ,以此类推。那么对于这种责任节点其实并没有什么顺序而言,对于一次输入每次有且仅有可能一个节点满足条件,而且实现类过多没必要在乎他的顺序,那么这个时候就需要我们使用圆形责任链模式了。
  • 环形链在双向链的基础上,我们只需要解决头尾两个节点,只需要将他们两个连接起来即可以实现环形链了。

注意事项

  • 环形责任链会有个致命的问题,那就是死循环

  • 所以为了避免死循环,我们在环形链中必须加入一个计数器。这个计数器的功能就是记录节点被执行的次数,在环形上的节点我们得保证每个节点执行的次数必须有一个上限值,这里我选择 1,即表示每个节点最多只能执行 1 次,关于这个计数器我们就可以借助 AbstractHandler 中抽离的父方法中进行拦截控制
public abstract class AbstractHandler implements StandoutHandler{
    private StandoutHandler nextHandler;
    private StandoutHandler preHandler;
    private int connectTimes = 0;
    private int closeTimes = 0;
    @Override
    public StandoutHandler getNextHandler() {
        return nextHandler;
    }

    @Override
    public void setNextHandler(StandoutHandler nextHandler) {
        this.nextHandler = nextHandler;
    }
    @Override
    public StandoutHandler getPreHandler() {
        return preHandler;
    }

    @Override
    public void setPreHandler(StandoutHandler nextHandler) {
        this.preHandler = nextHandler;
    }

    public abstract void runConnect(Request request);
    public abstract void runClose(Request request);

    @Override
    public void connect(Request request) {
        connectTimes++;
        if (connectTimes > 1) {
            return;
        }
        runConnect(request);
        if (this.nextHandler != null) {
            this.nextHandler.connect(request);
        }
    }
    @Override
    public void close(Request request) {
        closeTimes++;
        if (closeTimes > 1) {
            return;
        }
        runClose(request);
        if (this.preHandler != null) {
            this.preHandler.close(request);
        }
    }
}

责任链变形之通用责任链

  • 上面我们变形出来双向链、环形链都是从链条本身出发的,既然是发散思维我们就尽可能去发散,Client 方进行变形就会得倒各种各样组合,除此之外我们仔细观察下 UML 图示

  • 我们貌似忽略了指责链中最基本的一个对象那就是请求对象本身,假如我们现在针对订单存在一个责任链,针对用户对象又存在一个责任链。

  • 订单责任链、用户责任链实际上都是一个链表,那么他们区别点在哪呢?对就是责任链上每个节点处理的对象即我们之前谈及的 Request ,那么该如何实现通用责任链呢?其实很简单,我们只需要将方法中 Request 对象改为 Object 即可。

  • 因为改成通用责任链,那么我们在组装责任链的时候就需要注意将一类的节点组装到一起。

  • 为了避免组装这不清楚责任节点的功能,我们让每个节点提供它支持的范围,这样即使你将用户节点组装到了订单责任链上也没业务上的问题,顶多就是浪费一次循环机会。

责任链总结

  • 设计模式只是我们前辈们对代码的一种思想规范,我们要学习他们的思想但不能局限在设计模式下,要懂得结合实际业务进行适当的变通才真真正的王道!!!,比如我们理论知识只能学习到责任链模式实际上就是一个链表,来解决业务发起和业务实现解耦。
  • 单链表就是责任链模式的核心思想,但是我们不能一成不变一只单链表走天下,举一反三既然单链表是核心,那么我们就可以实现已双链表为核心的责任链模式,比如我们的连接释放场景。
  • 除了单链表、双链表我们还可以变形出环形链表的责任链。

源码中的责任链

源码追踪

  • 上面说了这么多无非都是对责任链的理解与变换,纸上得来终觉浅,接下来我们结合源码看看他们是如何运用责任链思想的。说到源码+责任链,我的脑海中立马浮现了如下一些关键词
关键词
Filter
StringBuilder
SpringMVC
Workflow
  • 接下来我们就从 SpringMVC 的源码中看看责任链是如何应用的。开始之前我们需要强调一点的是 SpringBoot 中请求的入口是 org.springframework.web.servlet.DispatcherServlet 这个类,接下来我们都是基于这个入口进行展开的,前置知识还得请读者自行补充下哦。

  • doDispatch 中我们能够看到开头会定义 4 个局部变量,这四个变量我们咋一看就能够看到 chain 而这个对应的就是 链条 的意思。这点可能有的读者会疑问,这样阅读源码也太随意了吧!!!,的确是,但是我自己在读这段代码的时候是全局看了大体逻辑,分别去看每一处的作用才发现 HandlerExecutionChain 是运用责任链模式的,只是现在向外输出的时候才以这种方式拉开入口的。
  • 我们看看 HandlerExecutionChain 这个类设计的方法调用

  • 我们发现在 dispatcher 方法中涉及了三处方法的调用。我们在看看 HandlerExecutionChain 这个类的代码吧。

  • 我们能够发现他的内部对象基本上和我们上面谈及的责任链模式中对象是吻合的,他的内部使用了 Java 的特性 List 来实现链表功能,而我们上述类似使用的 C 中的指针来实现链表的,实现方式不一样但是核心思想是一致的。

  • 接下来看看这些对象是如何构建的

  • 怎么样,他的构造函数实际上就是在初始化上面这些属性。和我们的责任链设计基本一致

  • 观看下他内部的方法基本上是和我们责任链中 AbstractHandler 对应的。因为我们主要看如何使用责任链模式,所以 HandlerExecutionChain 内部的方法我们暂时不需要过于关注。
  • 上面我们说了在 DispatcherServletHandlerExecutionChain 会使用到三个方法,他们分别是责任链上节点提供的三个同步业务的方法,其实分别就是对请求的不同阶段的一个拦截。三个方法内部都是一样的逻辑,分别执行责任链上每个节点对应的方法,HandlerInterceptor 接口在 web 开发中非常常用,里面有 preHandle()postHandle()afterCompletion() 三个方法,实现这三个方法可以分别在调用”Controller”方法之前,调用”Controller”方法之后渲染”ModelAndView”之前,以及渲染”ModelAndView”之后执行。所以这里我们只看其中 1 个方法的代码:

  • 不知道你看出来没有,在 HandlerExecutionChain 内部的 applyPreHanle 中和我们之前的 AbstractHandler 中 connect 方法基本上逻辑是一样的,都是遍历职责链上的节点进行执行。只不过在 HandlerExecutionChain 中上一个节点执行的结果会影响到是否继续路由到下一个节点执行。
  • 他的请求执行基本上和我们上面流程执行的逻辑一样

  • 所以说从代码结构上出发能够帮助我们快速理解逻辑。接下来我们在看看 dispatcher 这个方法的功能吧。

  • 上面是我将源码中的跟本次主题不相关的代码删除之后的伪代码,怎么样,这样看下来明白了 DispatcherServlet 在责任链中的作用了吧,他就是我们之前的 Test 类,可以理解成他就是实际的责任链使用者。
  • 回到责任链场景中,在 DispatcherServlet 中我们不需要关注 request 对象需要经过什么样的处理,我只关心我给了你 request , 你给我返回结果,就 OK 了。至于内部什么请求转发到 controller ,转发之前做权限验证、结果响应后统一格式等等需求都跟我没关系,需要实现实现我的 HandlerInterceptor 然后注册到我的责任链上就行了。
  • 其实对于 DispatcherServlet 来说业务非常的简单,请求拦截、发起请求、请求结束、数据封装就干了这四件事,其中三件需要借助我们的 HandlerExecutionChain 来完成。剩下的一件发起请求就涉及到我们的 RequestHandlerMapping 来处理请求映射问题。
  • 关于 HandlerAdapter Spring 使用的是适配器模式来实现的,至于适配器模式我们有专门的专题去讨论,这里我们简单梳理下 HandlerAdapter

  • 这个结构也非常的简单,其中只有 RequestMappingHandlerAdapter 有点特殊,因为他的处理比较麻烦。他主要是处理我们经常使用创建接口的方式,通过 RequestMapping 注解开启接口的映射。其他四种主要解决特定情况下的接口,大多都是实现指定类型的接口的方法,所以他们的方法比较固定,这点我们看看源码就能看出来。
适配器 作用
SimpleControllerHandlerAdapter 处理实现了 Controller 接口的 handler
HandlerFunctionAdapter 处理 HandlerFunction 类型的接口
HttpRequestHandlerAdapter 处理实现了 HttpRequestHandler 接口
SimpleServletHandlerAdapter 处理实现了 Servlet 接口的 handler
  • 好了,适配器我们就说这么多,总之我们就记住上面我们提到的 DispatcherServlet 中四个步骤,有三个步骤是利用责任链模式通过 HandlerInterceptor 来拦截执行的。

基于设计模式的思考

  • 也许你搭建过项目的基础架构接触过 HandlerInterceptor ,如果你没有接触过,进过今天从设计模式的角度简单的剖析了 DispatcherServlet 之后,你能明白 HandlerInterceptor 的使用场景吗?

应用场景

  1. 日志记录,可以记录请求信息的日志,以便进行信息监控、信息统计等。
  2. 权限检查:如登陆检测,进入处理器检测是否登陆,如果没有直接返回到登陆页面。
  3. 性能监控:典型的是慢日志。

SpringBoot 中自定义 HandlerInterceptor

  • 我们能看到在 SpringBoot 中一共有 15 个 HandlerInterceptor 的实现类。其中 HandlerInterceptorAdapter 是一个抽象类,这里还是需要提一嘴,这个是典型的接口适配器模式。他的好处我们不需要实现 HandlerInterceptor 接口的所有方法,我们只需要继承 HandlerInterceptorAdapter 并实现我们关心的方法,而不用像实现 HandlerInterceptor 那样实现接口中所有方法,这个我们在适配器模式中详细说说。
  • 所以想要在 SpringBoot 中实现自定义的 HandlerInterceptor 我们可以选择实现 HandlerInterceptor 也可以选择 extends HandlerInterceptorAdapter 类。
public class UserLoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("执行了拦截器的preHandle方法");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("执行了拦截器的postHandle方法");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("执行了拦截器的afterCompletion方法");
    }
}
  • 或者
public class TestFilter extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        logger.info("request请求地址path[{}] uri[{}]", request.getServletPath(),request.getRequestURI());
        return true;
    }
}
  • 准备好后我们只需要注册下即可。
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可添加多个,/**是对所有的请求都做拦截
        registry.addInterceptor(new TestFilter())
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/register");
        registry.addInterceptor(new UserLoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/register");
    }

    ....
}
  • 两种方式都可以,但是继承的方式在实现上比较简洁,你需要拦截什么事件就实现对应的拦截事件即可,更加的聚焦自己的业务。

完美退场

  • 天下无不散之宴席,到这里我相信你应该更加了解责任链模式了,同时应该也更加了解 SpringMVC 的处理流程了,当然这里也只是抛砖引玉,想要彻底完整的了解 SpringMVC , 光靠这点知识远远不够的。
  • 最后如果你觉得文章写的还不错,能够点赞+收藏呢?有了数据才能帮助更多的人,这样才能让你第一时间收到此类文章。

白嫖区

HandlerInterceptor:SpringBoot拦截器的基本使用(详解)_springboot之handlerinterceptor拦截器的使用详解_Java Punk的博客-CSDN博客

Spring Boot 实现登录拦截器(最强实战版) - Java技术栈 - 博客园

责任链模式在 Spring 中的应用 - 掘金

责任链设计模式(职责链模式)

责任链模式 | 菜鸟教程

java - 一起学设计模式 - 责任链模式 - 个人文章 - SegmentFault 思否

上一篇
坚持原创技术分享,您的支持将鼓励我继续创作!

评论系统未开启,无法评论!