文章标签:Java 设计模式 责任链模式
摘要:文件上传功能的检测环节需要校验扩展名、文件大小、敏感内容审核,随着业务扩展还要接入其他检测规则。如果把这些检测逻辑写在同一个方法里,每次新增检测规则都要修改核心业务代码,逻辑交织在一起难以单独测试。责任链模式把每种检测抽象成独立的"检测器",像安检通道一样串联起来,每个检测器只关注自己的职责,互不干扰。

一、问题场景:文件上传的“安检”越来越复杂

假设你在开发一个文件上传功能。上传的文件可能来自用户头像、商品图片、合同文档等不同场景,每种场景有不同的安全要求:

场景允许的扩展名文件大小上限敏感内容检测
用户头像jpg, png, gif2MB
商品图片jpg, png, webp5MB
合同文档pdf20MB

一开始需求简单校验扩展名就行了。你在 FileUploadService 里加了一个 validateFile 方法,三四行代码搞定。

第二周,产品说“不同场景的文件大小上限不一样”,你又往 validateFile 里加了一段大小判断。

一个月后,安全部门要求“对合同类文件进行敏感内容检测”。至此validateFile 方法已经变得臃肿不堪:

// ❌ 反面教材:一个方法承载了所有检测逻辑
public void validateFile(ProcessingContext context) {
    // 1. 扩展名校验
    Set<String> allowedExtensions = context.getSceneConfig().getAllowedExtensions();
    String extension = context.getFileMetadata().getExtension();
    if (!allowedExtensions.contains(extension)) {
        throw new FileException("FILE_EXTENSION_NOT_ALLOWED", extension);
    }

    // 2. 文件大小校验
    Long maxSize = context.getSceneConfig().getMaxFileSize();
    if (context.getFileMetadata().getFileSize() > maxSize) {
        throw new FileException("FILE_SIZE_EXCEEDED", maxSize);
    }

    // 3. 敏感内容检测
    if (context.getSceneConfig().isDetectionEnabled()) {
        // 调用检测平台接口检测
    }

    // 4. 未来可能还有:病毒扫描、文件名合法性...
}

即使你依然觉得没什么问题,大不了拆成几个小方法在validateFile中调用。但文件上传功能是一个基础功能,极有可能作为框架供其他项目使用,如果使用者需要自定义检测规则,又该如何?

这个写法的典型问题

  • 每次新增检测都要修改 validateFile 方法:违反了开闭原则(对扩展开放,对修改关闭)
  • 检测逻辑耦合在一起:扩展名校验和敏感内容检测完全不相干,却被塞在同一个方法体里
  • 无法独立测试:测试扩展名校验需要构造包含完整 SceneConfigFileMetadata 的上下文,即使你的测试目标只是"当扩展名不在白名单时抛异常"
  • 条件分支混杂isDetectionEnabled() 只在部分场景生效,导致方法内有大量 if-else 条件,可读性持续恶化

二、责任链模式:像安检通道一样处理请求

责任链模式(Chain of Responsibility)的核心思想很简单:

把每个处理步骤抽象为独立的处理器,按顺序串联成链。每个处理器只决定两件事:这个请求我管不管(isSupport),管的话怎么处理(detect)。

放在文件上传的场景里,就是给文件设置一条"安检通道":

文件到达 → 扩展名校验 → 文件大小校验 → 敏感内容检测 → 放行
             ↓            ↓           ↓
            通过         通过         通过

每个检测器都是独立的"安检员",只负责自己的那一道关卡。前面的通过了就交给下一个,任何一个不通过就直接拦截(抛出异常)。

三个核心角色

角色类/接口职责
处理器接口IFileDetector定义检测器的统一契约:是否支持此次检测 + 执行检测
具体处理器ExtensionDetectorSizeDetectorViolativeDetector各自实现一种具体的检测逻辑
责任链DetectionChain管理所有处理器、按优先级排序、遍历执行

三、责任链重构:给每个检测员一个独立工位

1. 定义处理器接口:IFileDetector

// 文件检测器接口,每个检测器都要实现的契约
public interface IFileDetector {

    /** 获取检测器名称,默认使用类名 */
    default String getName() {
        return this.getClass().getSimpleName();
    }

    /** 获取优先级,数字越小优先级越高,默认 0 */
    default int getPriority() {
        return 0;
    }

    /** 是否支持本次检测——根据上下文判断(如场景类型) */
    boolean isSupport(ProcessingContext context);

    /** 执行检测——不通过则抛出 FileException */
    void detect(ProcessingContext context) throws FileException;
}

关键设计点:

  • isSupport() 方法:让检测器自己决定"这次要不要参与",比如 ViolativeDetector 只有 敏感内容检测开启的场景才会生效,不需要在外部维护一份条件配置
  • getPriority() 方法:控制检测顺序——先做快速廉价的校验(扩展名、大小),再做开销大的检测,及早拦截不合格文件,避免无效计算
  • FileException 异常:检测失败的信号,责任链收到异常立即中断,不继续执行后续检测器

2. 实现三个具体的检测器

扩展名校验器:只关心"文件扩展名是否在场景允许的范围内"

public class ExtensionDetector implements IFileDetector {

    @Override
    public boolean isSupport(ProcessingContext context) {
        return true; // 所有场景都参与扩展名校验
    }

    @Override
    public void detect(ProcessingContext context) throws FileException {
        Set<String> allowedExtensions = Optional.ofNullable(context.getSceneConfig())
                .map(SceneConfig::getAllowedExtensions).orElse(Set.of());

        // 配了通配符 * 表示不限制扩展名
        if (allowedExtensions.contains("*")) {
            return;
        }

        String fileExtension = Optional.ofNullable(context.getFileMetadata())
                .map(FileMetadata::getExtension).orElse(null);
        if (!allowedExtensions.contains(fileExtension)) {
            throw new FileException(FileExceptionCode.FILE_EXTENSION_NOT_ALLOWED, fileExtension);
        }
    }
}

文件大小校验器:只关心"文件是否超过场景允许的最大值"

public class SizeDetector implements IFileDetector {

    @Override
    public boolean isSupport(ProcessingContext context) {
        return true; // 所有场景都参与大小校验
    }

    @Override
    public void detect(ProcessingContext context) throws FileException {
        Long maxFileSize = Optional.ofNullable(context.getSceneConfig())
                .map(SceneConfig::getMaxFileSize).orElse(0L);
        Long size = Optional.ofNullable(context.getFileMetadata())
                .map(FileMetadata::getFileSize).orElse(0L);

        if (size > maxFileSize) {
            throw new FileException(FileExceptionCode.FILE_SIZE_EXCEEDED, maxFileSize);
        }
    }
}

敏感内容检测器:只关心"文件是否包含违规信息"

public class ViolativeDetector implements IFileDetector {

    @Override
    public boolean isSupport(ProcessingContext context) {
        // 只有场景配置开启了敏感内容检测才参与
        return Optional.ofNullable(context.getSceneConfig())
                .map(SceneConfig::isDetectionEnabled)
                .orElse(Boolean.FALSE);
    }

    @Override
    public void detect(ProcessingContext context) throws FileException {
        // todo 调用敏感内容检测接口
    }
}

注意到 ViolativeDetector.isSupport() 返回的是条件判断——只有开启了敏感内容检测的场景才会执行这个检测器。这种"让处理器自己决定是否参与"的设计,避免了在责任链中维护庞大的条件路由表。

3. 实现责任链:DetectionChain

public class DetectionChain {

    private final List<IFileDetector> detectors = new CopyOnWriteArrayList<>();

    public DetectionChain(List<IFileDetector> detectors) {
        ListUtil.addAll(this.detectors, detectors);
        this.detectors.sort(Comparator.comparingInt(IFileDetector::getPriority));
    }

    /** 动态追加检测器 */
    public DetectionChain addDetector(IFileDetector detector) {
        if (detector != null) {
            this.detectors.add(detector);
            this.detectors.sort(Comparator.comparingInt(IFileDetector::getPriority));
        }
        return this;
    }

    /** 执行整条检测链——任一检测失败立即抛出异常 */
    public void execute(ProcessingContext context) throws FileException {
        for (IFileDetector detector : detectors) {
            if (!detector.isSupport(context)) {
                continue; // 不支持的检测器直接跳过
            }
            detector.detect(context);
        }
    }
}

核心流程就是一个遍历:按优先级排序后,逐个调用 isSupport() 判断是否参与,然后执行 detect()。任一检测器抛出异常,链终止。所有检测器都通过,文件放行。

4. 在业务代码中使用

// 构建检测链(一般在 Spring 配置或初始化阶段完成)
DetectionChain detectionChain = new DetectionChain(List.of(
    new ExtensionDetector(), // 扩展名检测
    new SizeDetector(),      // 文件大小检测
    new ViolativeDetector()  // 敏感内容检测
));

// 业务代码中一行调用
detectionChain.execute(context);

业务代码完全不需要知道链里有哪些检测器、它们的执行顺序是什么。检测链的组装和业务逻辑完全解耦。

5. 重构收益

维度重构前(一个方法承载所有逻辑)重构后(责任链模式)
新增检测规则修改 validateFile 方法,增加 if-else 分支新增一个实现 IFileDetector 的类,加到链中
修改现有规则在数百行的校验方法中定位、修改,影响其他逻辑只修改对应的 Detector 类,无需关心其他检测器
单元测试需要构造完整的上下文,测试方法受多个检测条件干扰每个 Detector 可独立测试,Mock 数据简单直接
排序调整调整代码顺序,存在遗漏风险修改 getPriority() 返回值即可
条件跳过在方法内写 if (xxEnabled) 嵌套判断isSupport() 中声明跳过条件,框架自动处理
代码可读性文件验证、大小验证、敏感内容检测逻辑交织在一起每个类职责单一,类名即文档

四、现代 Java 中的责任链进阶

Java 8 函数式简化

如果检测逻辑足够简单,可以直接用 Predicate 替代接口定义,不必为每个检测器创建一个类:

// 用 Predicate 定义检测规则
public class DetectionChain {

    private final List<Predicate<ProcessingContext>> detectors = new CopyOnWriteArrayList<>();

    public DetectionChain addDetector(String name, Predicate<ProcessingContext> detector) {
        this.detectors.add(detector);
        return this;
    }

    public void execute(ProcessingContext context) {
        for (Predicate<ProcessingContext> detector : detectors) {
            if (!detector.test(context)) {
                throw new FileException("DETECT_FAILED");
            }
        }
    }
}

// 使用方——直接传入 Lambda
DetectionChain chain = new DetectionChain();
chain.addDetector("extensionCheck", ctx -> {
    Set<String> allowed = ctx.getSceneConfig().getAllowedExtensions();
    return allowed.contains("*") || allowed.contains(ctx.getFileMetadata().getExtension());
});
chain.addDetector("sizeCheck", ctx ->
    ctx.getFileMetadata().getFileSize() <= ctx.getSceneConfig().getMaxFileSize()
);

这种做法适合简单规则,但项目中的检测逻辑通常涉及异常类型、错误码、国际化消息,用接口 + 实现类的方式更健壮。类有名字,Lambda 没有——当检测器需要明确的错误信息时,类更适合

五、经典应用:你其实早就见过

1. Servlet Filter —— Java Web 最经典的责任链

@WebFilter("/*")
public class AuthFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 前置处理:校验 Token
        if (!authenticate(request)) {
            response.sendError(401);
            return; // 拦截,不继续传递
        }
        // 调用下一个 Filter
        chain.doFilter(request, response);
        // 后置处理
    }
}

javax.servlet.FilterFilterChain 是责任链模式在 Java 生态中最广为人知的应用。每个 Filter 独立处理一种横切关注点(认证、日志、CORS、XSS 过滤等),FilterChain 负责按序传递请求。

2. Spring Security FilterChain

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http
            .addFilterBefore(new JwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(new RateLimitFilter(), JwtAuthFilter.class)
            .csrf(Customizer.withDefaults());
        return http.build();
    }
}

Spring Security 的 SecurityFilterChain 本质就是一条责任链,每个 Filter 负责一项安全职能(认证、授权、CSRF 防护、CORS 处理)。

3. MyBatis Plugin 拦截器链

MyBatis 的 Interceptor 机制也是责任链——多个插件可以拦截 ExecutorStatementHandlerParameterHandlerResultSetHandler 的执行,按 @Intercepts 注解配置的签名决定是否拦截。

六、使用指南:什么时候用,什么时候别用

✅ 适合用责任链模式:
   - 一个流程中有多个可独立变化的处理步骤
   - 处理步骤需要动态组合或调整顺序(如不同场景启用不同检测器)
   - 处理链可能在未来被扩展(新增检测规则)
   - 每个处理器需要自己决定"是否参与"(isSupport 模式)

❌ 不要硬套责任链:
   - 处理步骤固定不变,且不超过 2 个——简单的 if-else 更直接
   - 各个步骤之间有强依赖关系(步骤 B 必须使用步骤 A 的计算结果)
   - 处理步骤数量极少且永远不会增加(如永远只有一个检测器)

💡 重构黄金法则:
   当第三种检测需求出现时,考虑引入责任链。
   第一次出现变化,你可能只是加一个 if 分支;
   第二次出现变化,你开始警觉;
   第三次出现变化——这一刻就是引入责任链的最佳时机。
   模式是重构的产物,不是设计的前提。

七、总结

责任链模式解决的核心问题是:把一个流程中多个可独立变化的处理步骤拆开,让它们各自为政、互不干扰。

本文中的DetectionChain 是一个典型的责任链实现:

  • IFileDetector 定义了处理契约(isSupport+ detect
  • 三个 Detector 实现各自负责一种安全检测(扩展名、大小、敏感内容)
  • DetectionChain 管理排序和遍历,业务代码只关心"执行检测链"这一行调用

下次当你发现一个 validateXxxcheckXxx 方法越来越臃肿时,不妨想想:是不是该给每个检测员发一个独立的工位了?

更多推荐