Java责任链模式:文件上传的“安检通道”,每个检测器只管一件事
文章标签:
Java设计模式责任链模式
摘要:文件上传功能的检测环节需要校验扩展名、文件大小、敏感内容审核,随着业务扩展还要接入其他检测规则。如果把这些检测逻辑写在同一个方法里,每次新增检测规则都要修改核心业务代码,逻辑交织在一起难以单独测试。责任链模式把每种检测抽象成独立的"检测器",像安检通道一样串联起来,每个检测器只关注自己的职责,互不干扰。
一、问题场景:文件上传的“安检”越来越复杂
假设你在开发一个文件上传功能。上传的文件可能来自用户头像、商品图片、合同文档等不同场景,每种场景有不同的安全要求:
| 场景 | 允许的扩展名 | 文件大小上限 | 敏感内容检测 |
|---|---|---|---|
| 用户头像 | jpg, png, gif | 2MB | 否 |
| 商品图片 | jpg, png, webp | 5MB | 否 |
| 合同文档 | 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方法:违反了开闭原则(对扩展开放,对修改关闭) - 检测逻辑耦合在一起:扩展名校验和敏感内容检测完全不相干,却被塞在同一个方法体里
- 无法独立测试:测试扩展名校验需要构造包含完整
SceneConfig和FileMetadata的上下文,即使你的测试目标只是"当扩展名不在白名单时抛异常" - 条件分支混杂:
isDetectionEnabled()只在部分场景生效,导致方法内有大量if-else条件,可读性持续恶化
二、责任链模式:像安检通道一样处理请求
责任链模式(Chain of Responsibility)的核心思想很简单:
把每个处理步骤抽象为独立的处理器,按顺序串联成链。每个处理器只决定两件事:这个请求我管不管(isSupport),管的话怎么处理(detect)。
放在文件上传的场景里,就是给文件设置一条"安检通道":
文件到达 → 扩展名校验 → 文件大小校验 → 敏感内容检测 → 放行
↓ ↓ ↓
通过 通过 通过
每个检测器都是独立的"安检员",只负责自己的那一道关卡。前面的通过了就交给下一个,任何一个不通过就直接拦截(抛出异常)。
三个核心角色
| 角色 | 类/接口 | 职责 |
|---|---|---|
| 处理器接口 | IFileDetector |
定义检测器的统一契约:是否支持此次检测 + 执行检测 |
| 具体处理器 | ExtensionDetector、SizeDetector、ViolativeDetector |
各自实现一种具体的检测逻辑 |
| 责任链 | 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.Filter 和 FilterChain 是责任链模式在 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 机制也是责任链——多个插件可以拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 的执行,按 @Intercepts 注解配置的签名决定是否拦截。
六、使用指南:什么时候用,什么时候别用
✅ 适合用责任链模式:
- 一个流程中有多个可独立变化的处理步骤
- 处理步骤需要动态组合或调整顺序(如不同场景启用不同检测器)
- 处理链可能在未来被扩展(新增检测规则)
- 每个处理器需要自己决定"是否参与"(isSupport 模式)
❌ 不要硬套责任链:
- 处理步骤固定不变,且不超过 2 个——简单的 if-else 更直接
- 各个步骤之间有强依赖关系(步骤 B 必须使用步骤 A 的计算结果)
- 处理步骤数量极少且永远不会增加(如永远只有一个检测器)
💡 重构黄金法则:
当第三种检测需求出现时,考虑引入责任链。
第一次出现变化,你可能只是加一个 if 分支;
第二次出现变化,你开始警觉;
第三次出现变化——这一刻就是引入责任链的最佳时机。
模式是重构的产物,不是设计的前提。
七、总结
责任链模式解决的核心问题是:把一个流程中多个可独立变化的处理步骤拆开,让它们各自为政、互不干扰。
本文中的DetectionChain 是一个典型的责任链实现:
IFileDetector定义了处理契约(isSupport+detect)- 三个 Detector 实现各自负责一种安全检测(扩展名、大小、敏感内容)
DetectionChain管理排序和遍历,业务代码只关心"执行检测链"这一行调用
下次当你发现一个 validateXxx 或 checkXxx 方法越来越臃肿时,不妨想想:是不是该给每个检测员发一个独立的工位了?
更多推荐


所有评论(0)