设计模式学习(十一)责任链模式

目录

一、定义

责任链模式: 是一种行为设计模式,它可以将请求从一个对象传递到另一个对象,知道找到能够处理该请求的对象为止。

在责任链模式中,每个对象代表一个处理请求的节点,并持有一个指向下一个节点的引用。当一个请求进入责任链时,第一个节点会尝试处理该请求,如果该节点无法处理请求,则将请求传递给下一个节点。这个过程会一直持续下去,直到找到一个能够处理请求的节点或者整个链结束。

1.1 主要成员

  • 抽象处理者(Abstract Handler): 定义了处理请求的接口,并持有一个指向下一个处理者的引用。
  • 具体处理者(Concrete Handler): 实现抽象处理者接口,具体处理请求的逻辑。如果它无法处理请求,可以将请求传递给下一个处理者。
  • 客户端(Client): 创建责任链并将请求发送到责任链的第一个节点。

1.2 优点

1)解耦发起者和处理者: 发起者不需要知道处理请求的具体者,只需要将请求发送给责任链的第一个节点即可,而具体的处理者由责任链自动决定。

2)提高灵活性和扩展性:可以随时添加、修改或删除处理者节点,以满足不同的需求和业务场景。

3)可以动态地改变处理顺序:可以根据具体情况来灵活地调整节点的顺序,以适应不同的处理逻辑。

1.3 缺点

1)性能问题:由于责任链模式需要依次传递请求给每个节点,可能会导致处理时间比较长,特别是当责任链中的节点数量很大时。此外,节点的处理顺序也可能影响性能,如果节点的处理逻辑和顺序设计不好,可能会导致性能瓶颈。

2)无法保证请求被处理:责任链模式中,请求被传递给责任链中的节点,直到找到能够处理请求的节点为止。如果整个责任链都无法处理请求,那么请求可能会被无视或丢失。

3)可能导致调试困难:由于责任链模式将请求传递给多个节点进行处理,当出现问题时,可能会难以追踪到具体是那个节点处理出了问题。

4)责任链的长度和复杂性:责任链模式中,整个链路可能包含很多节点,特别是在复杂的业务需求下, 责任链的长度和复杂性可能会变得很高。这会使责任链的创建、维护和理解变得困难。

因此,在应用责任链模式时,需要权衡其优点和缺点,根据具体需求和场景来决定是否使用责任链模式以及如何进行设计和使用。


二、使用场景

模板模式策略模式责任链模式 这三种模式具有相同的作用:复用和扩展。在日常开发中,主要用于替换复杂的 if-else 分支判断。

2.1 Spring Security 中的应用

例如:Spring Security 中的过滤器链可以看作是一种责任链模式的实现。

在 Spring Security 中,存在一个特殊的过滤器链,用于处理 Web 请求的安全认证和授权。这个过滤器链由一系列的过滤器组成,每个过滤器都扮演者特定的角色和功能,如身份验证、授权处理、会话管理等。

当一个 Web 请求进入 Spring Security 的过滤链式,请求会依次经过每个过滤器进行处理。每个过滤器会根据自己的功能进行处理,并决定是否将请求传递给下一个过滤器。 如果当前过滤器无法处理请求,可以将请求传递给下一个过滤器,直到找到能够处理该请求的过滤器为止。

在这个过程中,每个过滤器都持有一个指向下一个过滤器的引用,形成了一个链式结构。这种方式可以 有效地解耦和组织处理逻辑,使得责任链中的每个过滤器都只需要关注自己的功能,而不需要关注整个过滤器链的处理过程


三、代码示例

3.1 场景及思路

需求:

  • 账号注册时进行校验,先后校验姓名、密码、手机号等。

使用责任链默认实现上述需求,可以消除很多 if-else 分支,增加功能的扩展性。

如果在责任链中增加一个校验,只需新建一个类即可,这个类就是责任链中的请求元素,可以选择性使用一个、多个或所有的请求对象。

责任链的具体实现方式有两种:

  • 链表式: 将下一个节点保存到当前节点的属性中,每次调用前通过 add() 设定下一个节点。
  • 数组式: 将所有节点保存到数组中,每次调用遍历数组。

我们这里以数组式为例,包结构如下:

3.2 实体类

UserInfo.java

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserInfo {

    /**
     * 姓名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;

    /**
     * 电话号码
     */
    private String phoneNumber;
}

3.3 抽象处理者

Verify.java

java 复制代码
import com.demo.entity.UserInfo;

public interface Verify {

    /**
     * 验证过程
     *
     * @param userInfo  用户信息
     * @param chain     下一个验证节点
     */
    void doVerify(UserInfo userInfo, VerifyChain chain);

}

3.4 具体处理者

1)责任链容器

VerifyChain.java (用 ThreadLocal 做线程隔离)

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;

import java.util.ArrayList;
import java.util.List;

public class VerifyChain {

    /**
     * 验证节点集合
     */
    private List<Verify> verifyList = new ArrayList<>();

    /**
     * 验证节点索引
     */
    private ThreadLocal<Integer> index = ThreadLocal.withInitial(() -> 0);

    /**
     * 添加验证节点
     */
    public VerifyChain addVerify(Verify verify) {
        verifyList.add(verify);
        return this;
    }

    /**
     * 开始验证
     * @param userInfo  用户信息
     */
    public void doVerify(UserInfo userInfo) {
        if (index.get() == verifyList.size()) {
            // 重置索引
            index.set(0);
            return;
        }

        System.out.println("当前线程:" + Thread.currentThread().getName() +
                ",索引:" + index.get() +
                ",验证节点:" + verifyList.get(index.get()).getClass().getSimpleName() +
                ",验证节点数量:" + verifyList.size());

        Verify verify = verifyList.get(index.get());
        verify.doVerify(userInfo, this);
        index.set(index.get() + 1);
    }
}
2)校验-用户名

UserNameVerify.java

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.util.StringUtils;

public class UserNameVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getUserName())) {
            System.out.println("用户名不能为空");
            return;
        }
        System.out.println("用户名验证通过");
        chain.doVerify(userInfo);
    }
}
3)校验-手机号

PhoneNumberVerify.java

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.util.StringUtils;

public class PhoneNumberVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPhoneNumber()) || userInfo.getPhoneNumber().length() != 11) {
            System.out.println("手机号码格式不正确");
            return;
        }
        System.out.println("手机号码验证通过");
        chain.doVerify(userInfo);
    }
}
4)校验-密码

PasswordVerify.java

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.util.StringUtils;

public class PasswordVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPassword())) {
            System.out.println("密码不能为空");
            return;
        }
        System.out.println("密码验证通过");
        chain.doVerify(userInfo);
    }
}

3.5 客户端(测试类)

java 复制代码
import com.demo.chain.PasswordVerify;
import com.demo.chain.PhoneNumberVerify;
import com.demo.chain.UserNameVerify;
import com.demo.chain.VerifyChain;
import com.demo.entity.UserInfo;

public class MainTest {

    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("ACGkaka", "123456", "12345678901");
        VerifyChain verifyChain = new VerifyChain();
        // 校验顺序:用户名 -> 密码 -> 电话号码
        verifyChain.addVerify(new UserNameVerify())
                .addVerify(new PasswordVerify())
                .addVerify(new PhoneNumberVerify());
        verifyChain.doVerify(userInfo, verifyChain);
    }
}

3.6 测试结果

单线程执行,可以看到,校验顺序与预期保持一致:用户名 -> 手机号码 -> 密码

可以调整代码中 addVerify 的顺序:手机号码 -> 用户名 -> 密码。

打开 thread2 注释多线程执行,可以看到,ThreadLocal生效,保证线程安全。

四、补充:SpringBoot 的 @Order 注解实现

需求:

  • 账号注册时进行校验,先后校验姓名、密码、手机号等。

4.1 实体类

UserInfo.java (保持不变)

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UserInfo {

    /**
     * 姓名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;

    /**
     * 电话号码
     */
    private String phoneNumber;
}

4.2 抽象处理者

Verify.java(保持不变)

java 复制代码
import com.demo.entity.UserInfo;

public interface Verify {

    /**
     * 验证过程
     *
     * @param userInfo  用户信息
     * @param chain     下一个验证节点
     */
    void doVerify(UserInfo userInfo, VerifyChain chain);

}

4.3 具体处理者

1)责任链容器

VerifyChain.java (用 ThreadLocal 做线程隔离,并且利用 Spring 自动注入责任链)

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

@Component
public class VerifyChain {

    /**
     * 验证节点集合(spring会根据 @Order 注解顺序注入)
     */
    @Resource
    private List<Verify> verifyList;

    /**
     * 验证节点索引
     */
    private ThreadLocal<Integer> index = ThreadLocal.withInitial(() -> 0);

    /**
     * 开始验证
     * @param userInfo  用户信息
     */
    public void doVerify(UserInfo userInfo) {
        if (index.get() == verifyList.size()) {
            // 重置索引
            index.set(0);
            return;
        }

        System.out.println("当前线程:" + Thread.currentThread().getName() +
                ",索引:" + index.get() +
                ",验证节点:" + verifyList.get(index.get()).getClass().getSimpleName() +
                ",验证节点数量:" + verifyList.size());

        Verify verify = verifyList.get(index.get());
        verify.doVerify(userInfo, this);
        index.set(index.get() + 1);
    }
}
2)校验-用户名

UserNameVerify.java (增加 @Component、@Order注解,来注入责任链并控制顺序)

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Order(1) // 数字越小,越先执行
@Component
public class UserNameVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getUserName())) {
            System.out.println("用户名不能为空");
            return;
        }
        System.out.println("用户名验证通过");
        chain.doVerify(userInfo);
    }
}
3)校验-手机号

PhoneNumberVerify.java (增加 @Component、@Order注解,来注入责任链并控制顺序)

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Order(2) // 数字越小,越先执行
@Component
public class PhoneNumberVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPhoneNumber()) || userInfo.getPhoneNumber().length() != 11) {
            System.out.println("手机号码格式不正确");
            return;
        }
        System.out.println("手机号码验证通过");
        chain.doVerify(userInfo);
    }
}
4)校验-密码

PasswordVerify.java (增加 @Component、@Order注解,来注入责任链并控制顺序)

java 复制代码
package com.demo.test.chain;

import com.demo.test.entity.UserInfo;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Order(3) // 数字越小,越先执行
@Component
public class PasswordVerify implements Verify {

    @Override
    public void doVerify(UserInfo userInfo, VerifyChain chain) {
        if (!StringUtils.hasText(userInfo.getPassword())) {
            System.out.println("密码不能为空");
            return;
        }
        System.out.println("密码验证通过");
        chain.doVerify(userInfo);
    }
}

4.4 客户端(测试类)

java 复制代码
package com.demo;

import com.demo.test.chain.VerifyChain;
import com.demo.test.entity.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootDemoApplicationTests {

    @Autowired
    private VerifyChain verifyChain;

    @Test
    void testVerify() {
        UserInfo userInfo = new UserInfo("ACGkaka", "123456", "12345678901");
        // 校验顺序:用户名 -> 手机号码 -> 密码
        Runnable runnable = () -> verifyChain.doVerify(userInfo);
        Thread thread1 = new Thread(runnable);
//        Thread thread2 = new Thread(runnable);
        thread1.start();
//        thread2.start();
    }

}

4.5 测试结果

单线程执行,可以看到,校验顺序与预期保持一致:用户名 -> 手机号码 -> 密码

可以调整 @Order 中的顺序:手机号码 -> 用户名 -> 密码。

打开 thread2 注释多线程执行,可以看到,ThreadLocal生效,保证线程安全。

整理完毕,完结撒花~ 🌻

参考地址:

1.责任链模式,https://zhuanlan.zhihu.com/p/509058039

2.【设计模式】责任链模式 ( 简介 | 适用场景 | 优缺点 | 代码示例 ),https://blog.csdn.net/shulianghan/article/details/118188083

3.SpringBoot中filter的使用详解及原理,https://blog.csdn.net/u014627099/article/details/84565603

4.用spring boot的@Order注解实现责任链模式,https://blog.csdn.net/qq_44993268/article/details/131020677?spm=1001.2014.3001.5501

相关推荐
金池尽干3 分钟前
设计模式之——观察者模式
观察者模式·设计模式
也无晴也无风雨17 分钟前
代码中的设计模式-策略模式
设计模式·bash·策略模式
数据与后端架构提升之路37 分钟前
从神经元到神经网络:深度学习的进化之旅
人工智能·神经网络·学习
一行11 小时前
电脑蓝屏debug学习
学习·电脑
星LZX1 小时前
WireShark入门学习笔记
笔记·学习·wireshark
阑梦清川1 小时前
在鱼皮的模拟面试里面学习有感
学习·面试·职场和发展
qq_433099401 小时前
Isaac Gym学习笔记——概述
学习
秃头佛爷3 小时前
Python学习大纲总结及注意事项
开发语言·python·学习
dayouziei5 小时前
java的类加载机制的学习
java·学习
dsywws8 小时前
Linux学习笔记之vim入门
linux·笔记·学习