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

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

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

场景 允许的扩展名 文件大小上限 敏感内容检测
用户头像 jpg, png, gif 2MB
商品图片 jpg, png, webp 5MB
合同文档 pdf 20MB

一开始需求简单校验扩展名就行了。你在 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 方法越来越臃肿时,不妨想想:是不是该给每个检测员发一个独立的工位了?

更多推荐