设计模式--责任链模式的思考

标准责任链模式

责任链模式: 客户端发出的请求,客户端本身并不知道被哪一个对象处理,而直接扔给对象链,该请求在对象链中共享,由对象本身决定是否处理. 当请求被处理后该链终止.本质目的是把客户端请求与接收者解耦,但是解耦的太彻底了,只能让接收者一个个来看看是不是自己该处理的请求.
标准的责任链模式一个请求只被一个对象处理,一旦处理成功后则链终止,请求不再被继续传递.标准的责任链模式并不是很通用,这种一对一模式大多场景可以用策略模式来代替,只有在客户端并不清楚具体的执行者是哪个对象的时候,责任链才比较适合.
举个例子:你想在天朝办理一个证,但是你不知道去哪比较好,因此你的选择就是一条链路,先去A局,A局让你去B局,B局让你去C局等等,直到解决你的问题,当然也存在白跑一趟的结果.这也是标准责任链的缺点,产生了太多没必要的调用.标准的责任链实际上应用场景并不是很多,而常使用的是升级版的功能链.

功能链

功能链是责任链的演变,结构上并没有实质的变化,只是每一个节点都可以处理请求,处理完转向下一个,也就是每一个请求都经历全部的链.这种应用场景就比较多了,比如我要办一件事,先去A再去B最后去C,这个例子还有点说明ABC三者的关系,取决于构造链时的顺序,另外每一步没处理好可以自由的选择退出链.
文字说的不是很理解,下面举几个实际中的代码实例.

Java中Filter链

对于Filter,其是由FilterChain来进行链的组合调用,请求的request与返回response实际上是共享的上下文信息,每一个处理的Filter都可以查看与修改.

1
2
3
4
public interface FilterChain {
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException;
}

在Tomcat中实现类为org.apache.catalina.core.ApplicationFilterChain,其结构如下图:

其中数组filters就是所谓的filter链,利用pos(当前执行到的位置)与n(filter链长度)来进行链的调用.
那么怎么让链节点选择继续执行还是停止执行呢?答案的FilterdoFilter方法,该方法把责任链作为参数FilterChain一直传递下去,继续就调用chain的doFilter方法,不继续则不调用.

1
2
void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;

Spring Security中拦截链

Spring Security中的链实际上是由SecurityFilterChain接口所定义,其很简单的就是暴露出一个list的链.其中matches判断请求是否是这条链来处理.

1
2
3
4
5
6
public interface SecurityFilterChain {

boolean matches(HttpServletRequest request);

List<Filter> getFilters();
}

在其入口处类org.springframework.security.web.FilterChainProxy中包含着多条链.

1
private List<SecurityFilterChain> filterChains;

每一个链处理的是一类请求.Spring Security使用简单的for循环判断定位到具体执行的链.

1
2
3
4
5
6
7
8
private List<Filter> getFilters(HttpServletRequest request)  {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}

那么这个结构因为这个设计模式就很清晰了(这也是熟悉了设计模式之后的优势,看源码可以有一种全局把控感)

还有一个问题,链是如何自由执行的?
这一点与Java Filter一模一样,Spring Security实现了一个org.springframework.security.web.FilterChainProxy.VirtualFilterChain类,该类同样实现了FilterChain接口,里面的调用逻辑也与tomcat方式一致.具体就不讨论了.

另外Spring Security也提供了一种数据共享的方式,利用ThreadLocal保证线程安全,达到共享数据的目的.另外这里的SecurityContextHolderStrategy是策略模式的一种应用,值得一看.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {

private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();

public void clearContext() {
contextHolder.remove();
}

public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();

if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}

return ctx;
}
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}

public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}

Mybatis中插件链

Mybatis中插件使用的是类似责任链的一种模式,当然也可以称之为责任链模式,毕竟思想都是类似的.其中插件是通过Interceptor接口实现的,其中plugin方法就是为目标对象套上该链的一个节点.

1
2
3
4
5
6
7
8
public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);
}

那么如何构造这个链?在InterceptorChain中有如下方法,InterceptorChain是在构造配置时组装好的,燃后对目标使用pluginAll方法,构造完整链.

1
2
3
4
5
6
7
8
9
10
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
...
}

其中plugin官方推荐Plugin.wrap(target, this)方法,该方法本质上是用代理模式嵌套住目标类

1
2
3
4
5
6
7
8
9
10
11
12
13

public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

那么这种构造出的链大概如下面这种嵌套结构,这种链可以说是彻底的功能链,其一旦组装好就无法变化了.当然这种也适合Mybatis这种从配置中就定死了执行链.

业务开发中可以常用到的链

在业务开发中常常能遇到这类需求,比如退款操作,退款后可以恢复商品库存,恢复活动库存,退掉用户的优惠券,退掉用户的活动资格等等,该一系列的操作就是一条线性链,那么就可以利用责任链的思想来完成在这样的需求.
先提取出一个公共接口,链节点实现该接口,完成具体的退款操作

1
2
3
4
5
6
7
8
9
public interface RegainAfterRefundOrder {
/**
* 退回操作
* @param bo 该订单,可能是子订单,也可能是主订单,自行判断
* @param operator 操作人
* @return true成功
*/
boolean regain(BizOrderDO bo, Long operator);
}

接下来是链的统一管理,也就是需要Chain这个类来管理,可以按照下面的实现,其调用链只是简单的在applyAllPlugin循环调用,该过程可以按照Spring Security等方式实现更加灵活的调用.
可以根据需求设计为一旦创建就不可改变的类,包括类中的interceptors,这样使得代码更加健壮.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class RefundOrderAndRegainChain {

private final List<RegainAfterRefundOrder> interceptors = new ArrayList<>();

public void applyAllPlugin(BizOrderDO bo, Long operator) {
for (RegainAfterRefundOrder interceptor : interceptors) {
interceptor.regain(bo, operator);
}
}

public void addInterceptor(RegainAfterRefundOrder interceptor) {
interceptors.add(interceptor);
}

public List<RegainAfterRefundOrder> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}

最后是借助IOC实现链的组装,假设有RegainCouponRegainInventoryCountRegainInvitationCodeWithDraw等RegainAfterRefundOrder的实现类,依次在Spring的Configuration类中实现注入,并构造出需要的RefundOrderAndRegainChain.最后再业务需要的地方直接注入该Chain即可.对于这种逻辑实现了解耦与灵活的组合.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class RefundOrderAndRegainConfig {

@Bean
public RefundOrderAndRegainChain paidToRefund(
RegainInventoryCount regainInventoryCount,
RegainCoupon regainCoupon,
RegainPromotionRegistered regainPromotionRegistered,
RegainInvitationCodeWithDraw regainInvitationCode) {
RefundOrderAndRegainChain chain = new RefundOrderAndRegainChain();
chain.addInterceptor(regainInventoryCount);
chain.addInterceptor(regainPromotionRegistered);
chain.addInterceptor(regainCoupon);
chain.addInterceptor(regainInvitationCode);
return chain;
}

}

责任链处理的副作用

上面几个小案例可以看出链式处理方式能够将一段复杂逻辑拆分开来,然后使用链的方式组合,那么这种方式有什么副作用?

1. 假如入参是可变更对象

由于接口契约的限制,每一个链中的节点,所接收到的都是同一个参数,此时如果参数是可变更对象,一旦其中一个节点修改了入参,那么会导致下一个节点接收到的对象与其他节点不一致。从开发角度并没有限制这种做法,但从实际开发角度来说,一旦出现了问题,这种做法会导致排查难度上升到另一个量级,针对可变更对象作为入参,更适合的应该是状态机,而不是责任链。

2. 假如节点之间存在依赖关系

节点之间存在依赖关系,是链式设计过程中经常遇到的问题。首先博主认为该问题不可避免,因此应该说说如何更好的处理这一类问题。节点之间存在依赖关系,首先要考虑的点是这个链的组合编排要固定,不能每次启动后不一致,比较简单的做法就是在Spring bean注册时,就将这部分顺序逻辑固定死的注册好。之后需要考虑依赖的节点失败了会怎么样,比如上述的退款业务链的设计,假设优惠券退成功了,但后续的库存退失败了,这个失败异常很可能到链的外层,从而引起整条链的重试,那么就需要考虑节点的幂等性设计是否合理。

责任链模式的本质

  1. 让请求者不关心具体接收者是谁,只需要得到自己的具体结果
  2. 在一个请求对应多个接收者情况下(Spring Security这种),接收者之间可以自由组合,灵活性很高
  3. 新增接收者处理也只需要增加链中的一个节点,不需要改动太多.
设计模式--策略模式的思考
设计模式--模板方法模式的思考