news 2026/9/7 9:48:38

责任链模式详解:从if else到优雅的审批链实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
责任链模式详解:从if else到优雅的审批链实现

大家好,我是你们的技术博主。今天我们聊一个在业务开发中出镜率奇高、但许多初学者一看到类名就发懵的设计模式——责任链模式。

之前在项目里接到一个需求:不同金额的采购单要走不同的审批流程;请假超过 3 天要总经理批,3 天以内部门经理就能定。第一版代码我写了一大串if ... else if ...,每次新增一个审批角色,就要改动核心方法,改到后来自己都分不清哪条分支生效。后来重构时换成责任链模式,不仅代码清爽了,后续加审批节点也只需要新增一个类,不再动老代码。

这篇文章就从实际场景切入,把责任链模式的原理、代码实现、典型应用、框架中的踪迹、常见坑点,一次性讲透。文章内容比较多,建议收藏后静下心来看。

1. 责任链模式是什么?解决什么问题?

1.1 从现实问题说起:if else 的噩梦

先来看一个非常常见的业务场景:请假审批。

假设现在公司规定是这样的:

  • 请假天数小于等于 2 天,由组长审批;
  • 请假天数大于 2 天且小于等于 5 天,由部门经理审批;
  • 请假天数大于 5 天,由总经理审批。

很多人的第一反应是写这样一个方法:

public class LeaveService { public String approve(LeaveRequest request) { int days = request.getDays(); if (days <= 2) { return "组长审批通过"; } else if (days <= 5) { return "部门经理审批通过"; } else { return "总经理审批通过"; } } }

这段代码在需求固定的情况下没有任何问题。但业务是不断变化的,很快产品经理又提了新需求:

  • 请假 10 天以上需要 CEO 审批;
  • 每天请假人数超过 50 人时,需要 HR 部门备案。

于是你的if else越来越多,方法越来越长。更糟糕的是,如果审批顺序发生变化,比如“先由 HR 备案,再由组长审批”,你不得不继续改写这个方法的内部结构。

这种写法的核心问题在于:请求的接收者和处理者被硬编码在了一起,每新增一种处理逻辑,都要修改已有代码,违反了开闭原则(对扩展开放,对修改关闭)。

1.2 责任链模式的定义

责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式。它的核心思想是:

将能够处理同一类请求的多个对象连成一条链,提交一个请求沿链传递,链上的每个对象依次判断自己能否处理这个请求。如果能处理,就处理;如果不能处理,就把请求转发给下一个对象。

换一句更通俗的话:避免请求发送者与多个处理者之间产生强耦合,让多个对象都有机会处理请求,并且由链式结构动态决定请求最终由谁处理。

这里有一个关键点:发送者并不知道也不关心最终是哪个对象处理了请求,它只需要把请求丢到链上即可。处理顺序、处理策略完全由链条的构建方式决定。

1.3 责任链模式的四个核心角色

责任链模式通常包含 4 个角色:

角色职责
抽象处理者(Handler)定义处理请求的接口,并且持有下一个处理者的引用
具体处理者(ConcreteHandler)实现抽象处理者,判断自己能否处理请求,不能处理则交给下一个
请求对象(Request)封装请求数据
客户端(Client)创建责任链,并向链头发送请求

抽象处理者的核心定义如下:

public abstract class Approver { protected Approver next; public void setNext(Approver next) { this.next = next; } // 由子类实现具体的审批逻辑 public abstract void handle(LeaveRequest request); }

每个具体处理者都持有下一个处理者的引用,形成一条单向链表。这也就是“责任链”名字的由来。

2. 责任链模式的两种实现方式

责任链模式在代码实现上,可以分成两种常见形式:链表式责任链和数组式责任链。它们各有适用场景。

2.1 链表式责任链

链表式是责任链模式的经典实现,也是 GoF 设计模式书中描述的形态。

每个处理者内部持有下一个处理者的引用,通过setNext()方法串联。当处理完当前节点后,如果需要传递,就调用next.handle(request)

它的优点是:

  • 结构直观,代码好理解;
  • 链的长度可以动态调整;
  • 可以在链中间插入或删除节点。

缺点是:

  • 如果链很长,调用栈会比较深;
  • 每个节点都要维护 next 引用,职责上有一点冗余。

链表式责任链适用于处理节点数量相对固定、调用链比较清晰的场景,比如请假审批、报销审批。

2.2 数组式责任链

数组式责任链,也叫“列表式责任链”,是把所有处理者放在一个List中,通过索引遍历。

List<Handler> handlers = new ArrayList<>(); handlers.add(new GroupLeaderHandler()); handlers.add(new ManagerHandler()); handlers.add(new BossHandler()); for (Handler handler : handlers) { if (handler.canHandle(request)) { handler.handle(request); break; } }

这种方式在业务系统中使用得也非常广泛,尤其是在中间件、拦截器、过滤器的设计中。

它的优点是:

  • 链路配置非常灵活,可以利用 Spring 的依赖注入自动收集所有处理器;
  • 便于实现“某个处理器不满足条件就跳过”的便捷逻辑;
  • 调用栈浅,调试方便。

缺点是:

  • 每个处理者仍然需要具备canHandle判断能力,否则语义不清;
  • 如果删除某个节点,需要操作 List 而不是节点本身。

在实际工程中,两种方式可以结合。比如用数组方式收集和排序所有处理器,内部处理时再通过“链式调用”把请求传递下去。

3. 责任链的传递与控制:三种请求处理策略

很多初学者只知道责任链是“一条链顺着往下传”,但不知道其实责任链有三种常见的处理策略。理解了这三种策略,你才能在不同的业务需求中选择正确的实现方式。

3.1 策略一:全链路遍历,每个节点都处理

这种策略下,请求从链头开始,每个处理者都会执行自己的逻辑,直到链结束。适用于“日志记录”、“参数校验”、“敏感词过滤”等场景。

比如日志框架中,一条日志可能要经过格式化、过滤、输出等多个环节,每个环节都需要参与:

public class LogInterceptorChain { private List<Interceptor> interceptors; private int index = 0; public void proceed(LogRequest request) { if (index >= interceptors.size()) { return; } Interceptor interceptor = interceptors.get(index++); // Do something interceptor.intercept(request, this); } }

每个拦截器处理完自己的事情之后,再调用chain.proceed()把请求传给下一个。这就是“全链路都要处理”的典型实现。

3.2 策略二:找到第一个能处理的节点,立即停止

这种策略对应我们之前说的审批场景。请求从链头开始,遇到第一个具备处理能力的节点,就由该节点处理,后面的节点不再执行。

public abstract class Handler { protected Handler next; public void handle(Request request) { if (canHandle(request)) { doHandle(request); } else if (next != null) { next.handle(request); } else { // 无人处理 throw new UnsupportedOperationException("没有处理器"); } } protected abstract boolean canHandle(Request request); protected abstract void doHandle(Request request); }

这种策略的核心是canHandle()判断逻辑。每个节点自己决定“我能不能处理这个请求”,不能则向下传递。优点是请求在遇到正确节点后立即返回,执行效率高;缺点是如果链条构建顺序有误,请求可能被错误节点提前拦截。

3.3 策略三:可中断的责任链

这是“全链路遍历”的进阶版。每个节点都处理请求,但任何一个节点都可以主动中断传递。这种策略广泛应用于网关、拦截器、认证授权等场景。

比如 Spring MVC 的拦截器HandlerInterceptorpreHandle返回false就中断了后续拦截器和 Controller 的执行:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; // 中断链路 } return true; } }

这里返回的boolean就决定了链路是否继续向下传递。这种方式比“逐层向下传递”更加灵活,也更符合 Web 应用的现实需求。

4. 完整实战案例:请假审批系统

理论讲完了,接下来我们通过一个完整的请假审批系统案例,把责任链模式从头到尾写一遍。为了让代码更贴近真实工程,我会使用 Spring Boot 的组件扫描思想来装配责任链,同时也会给出手动构建链的版本。

4.1 项目结构与依赖

我们的项目是一个普通的 Maven Java 项目,不需要额外的第三方框架。JDK 版本建议 8 以上。

chain-of-responsibility-demo/ ├── pom.xml └── src/main/java/com/example/demo/ ├── LeaveRequest.java ├── Approver.java ├── GroupLeaderApprover.java ├── ManagerApprover.java ├── BossApprover.java └── Client.java

pom.xml只需要基本配置即可:

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>chain-of-responsibility-demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>

4.2 定义请求对象

首先定义请假请求对象,封装请假人的信息和请假天数。

package com.example.demo; /** * 请假请求 */ public class LeaveRequest { private String name; private int days; public LeaveRequest(String name, int days) { this.name = name; this.days = days; } public String getName() { return name; } public int getDays() { return days; } }

这里只保留了两个核心字段:申请人姓名name和请假天数days。你完全可以按业务需要扩展,比如请假类型、开始时间、结束时间、原因等。

4.3 定义抽象处理者

抽象处理者规定了责任链的基本骨架。

package com.example.demo; /** * 抽象审批人 */ public abstract class Approver { protected Approver next; /** * 设置下一个审批人 */ public void setNext(Approver next) { this.next = next; } /** * 审批方法:由子类实现具体的审批规则 */ public abstract void handle(LeaveRequest request); }

在抽象类中,我们只做了两件事:

  1. 维护next引用,用于指向链上的下一个处理者。
  2. 声明抽象方法handle,由具体的审批人实现。

这里的一个细节是:抽象处理者既可以是抽象类,也可以是接口。在 Java 8+ 中,接口可以包含default方法,也能实现类似的功能。但如果是审批这种“持有下一个引用”的模型,抽象类往往更直观,因为下一个节点的引用状态可以统一放在抽象类中管理。

4.4 定义具体处理者

接下来实现三个具体审批人:组长、部门经理、总经理。

组长审批:

package com.example.demo; /** * 组长审批:2天以内 */ public class GroupLeaderApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() <= 2) { System.out.println(request.getName() + " 请假 " + request.getDays() + " 天,组长审批通过。"); } else { // 超出权限范围,交给下一个审批人 if (next != null) { next.handle(request); } else { System.out.println("请假天数过多,无人审批。"); } } } }

部门经理审批:

package com.example.demo; /** * 部门经理审批:2天以上,5天以内 */ public class ManagerApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() > 2 && request.getDays() <= 5) { System.out.println(request.getName() + " 请假 " + request.getDays() + " 天,部门经理审批通过。"); } else { if (next != null) { next.handle(request); } else { System.out.println("请假天数过多,无人审批。"); } } } }

总经理审批:

package com.example.demo; /** * 总经理审批:5天以上 */ public class BossApprover extends Approver { @Override public void handle(LeaveRequest request) { if (request.getDays() > 5) { System.out.println(request.getName() + " 请假 " + request.getDays() + " 天,总经理审批通过。"); } else { System.out.println("流程结束,未匹配到合适的审批人。"); } } }

你会发现,每个具体处理者的代码结构非常相似:先判断自己有没有权限处理;有则处理,没有则传递给下一个节点。这就是责任链模式中“每个处理者自己决定自己能否处理”的设计思想。

4.5 构建责任链并运行验证

最后写一个客户端来组装责任链。

package com.example.demo; /** * 客户端:组装责任链并发送请求 */ public class Client { public static void main(String[] args) { // 1. 创建审批人节点 Approver groupLeader = new GroupLeaderApprover(); Approver manager = new ManagerApprover(); Approver boss = new BossApprover(); // 2. 组装责任链:组长 -> 部门经理 -> 总经理 groupLeader.setNext(manager); manager.setNext(boss); // 3. 发送请求,链头是组长 System.out.println("=== 请假 1 天 ==="); groupLeader.handle(new LeaveRequest("张三", 1)); System.out.println("=== 请假 3 天 ==="); groupLeader.handle(new LeaveRequest("李四", 3)); System.out.println("=== 请假 7 天 ==="); groupLeader.handle(new LeaveRequest("王五", 7)); } }

运行结果:

=== 请假 1 天 === 张三 请假 1 天,组长审批通过。 === 请假 3 天 === 李四 请假 3 天,部门经理审批通过。 === 请假 7 天 === 王五 请假 7 天,总经理审批通过。

可以看到,客户端只需要把请求交给链头,至于请求最终由谁处理、处理规则是什么,客户端完全不需要关心。这是责任链模式最核心的价值。

这里有一个容易忽略的细节:BossApprover是最末端的节点,它后面已经没有next了。所以它的逻辑里不管匹配不匹配,都要给流程一个明确的结尾。实际项目中的末端节点,通常是一个兜底处理器:要么处理请求,要么抛出异常。

5. 责任链模式在主流框架中的应用

责任链模式从来不只是教科书里的理论,它在主流框架中广泛存在。理解框架里的责任链,能帮你更好地配置框架、排查问题。

5.1 Servlet Filter 过滤器链

Java Web 开发中,Filter就是一条典型的责任链。多个过滤器通过FilterChain串联起来,每个过滤器决定是否放行请求。

public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); // 放行,继续执行下一个过滤器或目标资源 chain.doFilter(request, response); } }

chain.doFilter()就相当于责任链中的next.handle()。如果过滤器不调用doFilter(),请求就被拦截在当前节点。

5.2 Spring Security 的过滤器链

Spring Security 的安全机制,本质上是建立在 Servlet Filter 之上的一个大型责任链。SecurityFilterChain中包含了很多默认的Filter,比如:

  • UsernamePasswordAuthenticationFilter:处理表单登录;
  • BasicAuthenticationFilter:处理 Basic 认证;
  • ExceptionTranslationFilter:转换异常;
  • FilterSecurityInterceptor:做最终的授权判断。

你可以通过HttpSecurity在链上任意位置插入自定义过滤器:

@Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }

理解这一点对排查 Spring Security 问题非常有帮助。比如“登录接口为什么被拦截”,本质上就是责任链上的某个过滤器没有放行请求。

5.3 MyBatis 的插件机制

MyBatis 的插件(Plugin)也是基于责任链思想实现的。MyBatis 允许你在 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四个核心对象上插入拦截逻辑。多个插件会组成一条链,每个插件在调用目标方法时按顺序执行。

@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class MyPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 前置逻辑 System.out.println("执行 SQL 前"); Object result = invocation.proceed(); // 后置逻辑 System.out.println("执行 SQL 后"); return result; } }

这里的invocation.proceed()就是“调用链中的下一个节点”。

5.4 Netty 的 ChannelPipeline

Netty 是一个高性能网络框架,它的ChannelPipeline与责任链模式一脉相承。每个入站或出站事件都会在 ChannelPipeline 中依次经过所有ChannelHandler

pipeline.addLast(new HttpRequestDecoder()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new MyBusinessHandler());

事件从第一个 Handler 开始,逐个向后传递。任一个 Handler 都可以决定事件是否继续传播。Netty 的ChannelHandlerContext.fireChannelRead()方法,就是链式传递的体现。

除了上面这几个框架,Dubbo 的过滤器机制、OkHttp 的拦截器(Interceptor)、Spring 的HandlerInterceptor、日志框架的 Appender 链,到处都能看到责任链模式的身影。

6. 责任链模式常见问题与设计误区

在实际开发中,责任链模式如果用得不好,反而会给项目带来维护负担。下面这几个问题是我见过的高频坑点。

6.1 请求没有处理者,直接石沉大海

如果在责任链的末端没有兜底逻辑,请求就不声不响地结束了。这在审批、风控等强业务场景中非常危险。

解决思路:在链路末端增加一个兜底处理者。它可以抛出异常、记录告警日志,或者返回一个默认结果。

public class DefaultApprover extends Approver { @Override public void handle(LeaveRequest request) { throw new UnsupportedOperationException("没有审批人能够处理该请求"); } }

6.2 责任链出现环形引用

如果 A 的 next 指向 B,B 的 next 指向 A,请求就会在 AB 之间无限循环,最终导致栈溢出。

解决思路:构建责任链时保持单向性。不要手写环形引用,尽量通过工程手段统一构建链路。例如用列表来维护处理者,再循环设置 next,让环形引用从结构上无法出现。

6.3 处理顺序依赖构建顺序,容易出错

责任链的执行结果与链的构建顺序强相关。同一个请求,如果先经过“组长”再经过“经理”,和反过来执行,结果完全不同。

解决思路:在文档或注释中明确链路的顺序,最好在配置中心或数据库表中维护顺序,让非开发人员也能调整。

6.4 滥用责任链,把简单逻辑复杂化

一个只有两层判断的逻辑,强行套责任链,只会让代码变多、阅读变难。责任链适合处理“处理者数量可能动态变化”的场景。如果你的节点是稳定的两三个,用简单策略模式或 if else 反而更好。

判断标准:当新增一种处理者不需要修改已有代码时,才是责任链模式发挥最大价值的时候。

6.5 在责任链中修改共享请求对象

多个处理者同时修改请求对象,会导致下一节点拿到的数据和上一节点看到的不一致。这种隐式状态修改很难排查。

解决思路:请求对象尽量设计为不可变对象;如果确实需要传递中间结果,可以封装一个独立的上下文对象。

7. 责任链模式最佳实践与工程建议

结合我平时写业务代码的经验,这里分享几条责任链模式的实战建议。

7.1 用 Builder 模式构建责任链,屏蔽组装细节

当责任链节点较多时,客户端里写一串setNext()会非常啰嗦。推荐引入 Builder 模式:

public class ApproverChainBuilder { private Approver head; private Approver tail; public ApproverChainBuilder add(Approver approver) { if (head == null) { head = approver; tail = approver; } else { tail.setNext(approver); tail = approver; } return this; } public Approver build() { return head; } }

客户端调用:

Approver chain = new ApproverChainBuilder() .add(new GroupLeaderApprover()) .add(new ManagerApprover()) .add(new BossApprover()) .build();

这样责任链的顺序就非常直观,后续增删节点也方便。

7.2 配合 Spring 依赖注入,自动收集处理者

在 Spring Boot 项目中,你可以让所有具体处理者成为 Spring Bean,然后通过List注入自动收集:

@Component public class ApproverChainConfig { @Autowired private List<Approver> approvers; @Bean public Approver approverChain() { // 按 @Order 排序 approvers.sort(Comparator.comparingInt(this::getOrder)); return buildChain(approvers); } }

这样每新增一个审批角色,只需要写一个新的Approver实现类并注册为 Bean,完全不需要修改现有代码。这正是责任链模式“对扩展开放,对修改关闭”的完美体现。

7.3 结合链路追踪,为每个处理节点记录日志

责任链的调试难点在于:你不知道请求当前到了哪个节点、在哪一步被卡住了。建议在每个处理者的handle方法里打印清晰日志,或者在请求对象中维护一个已处理节点列表。

public abstract class Approver { protected Approver next; public void handle(LeaveRequest request) { request.addTrace(getClass().getSimpleName()); doHandle(request); } protected abstract void doHandle(LeaveRequest request); }

这样即使链路出问题,你也可以根据 trace 信息快速定位是哪个节点出了问题。

7.4 控制责任链长度,避免性能损耗

责任链模式虽然优雅,但不是越长越好。每个节点的判断都有开销。如果链上有几十个节点,而每个节点只做一点简单的判断,性能就会受到不必要的影响。对于性能敏感的场景,可以用“短路设计”,让前置节点快速过滤掉大部分请求,避免请求走到很深的链路。

7.5 明确每个节点的单一职责

一个处理者只做“一件事”。比如“参数校验”的节点只做校验,“敏感词过滤”的节点只做过滤。如果某个节点既要校验又要改数据还要记日志,职责就太混杂了,未来极难维护。

8. 总结

责任链模式通过将多个处理者串成一条链,解耦了请求的发送者和接收者。请求沿着链路传递,每个节点自行决定是处理请求还是转发请求。这种设计带来的最大收益是:新增处理逻辑时不需要修改原有代码,只需要新增节点并重新组装链路,符合开闭原则。

在实际开发中,责任链模式广泛应用于审批流、日志记录、过滤器、拦截器、认证授权、网关路由、插件机制等场景。Servlet Filter、Spring Security、MyBatis Plugin、Netty ChannelPipeline、OkHttp Interceptor 等框架机制中,都能看到责任链模式的影子。

只要你记住下面三点,责任链模式就能成为你工具箱里趁手的工具:

  • 使用场景:处理者可能会动态变化,且请求需要被多个对象依次尝试处理。
  • 核心关键:每个节点自行判断处理或转发,链的组装与请求发送解耦。
  • 易犯错误:没有兜底节点、链路顺序依赖构建顺序、滥用导致代码复杂。

下一步你可以尝试把项目中的一段if else逻辑重构成责任链模式,感受一下变更成本的变化。如果这篇文章对你有帮助,欢迎点赞、收藏、转发。你的支持是我持续输出技术干货最大的动力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:48:16

AI漫剧制作全流程拆解:从剧本生成到成片量产的关键技术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:45:45

五步打造可复用AI Agent Skill:从Prompt到能力建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:45:44

用Excel VBA打造年会抽奖程序:滚动动画与等概率不重复抽取

简介&#xff1a;一份面向企事业单位年会、春节联欢等场景的Excel VBA抽奖程序&#xff0c;由Excel工作表与宏代码实现&#xff0c;适合需要快速搭建现场抽奖环节的行政、HR或活动组织者使用。程序内置特等奖至五等奖及自定义奖项&#xff0c;支持按奖项级别批量抽取、现场弃权…

作者头像 李华
网站建设 2026/9/7 9:44:18

DMA+SG+FIFO实战:从描述符链表到串口空闲中断的高效数据搬运

简介&#xff1a;DMA_SG_FIFO.zip 是一份基于 Vivado 2017 的 FPGA 工程资源&#xff0c;面向使用 Xilinx AX7015&#xff08;Kintex-7 系列&#xff09;的开发者&#xff0c;演示如何通过 AXI DMA IP 核的 Scatter-Gather 模式实现高效数据搬运&#xff0c;并结合 FIFO 缓存优…

作者头像 李华