GoF 定义
- Chain Of Responsibility Design Pattern
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
借由<设计模式之美>中王争的通俗解释就是:
"在职责链模式中,多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。"
传统意义上的责任链模式有几个特点:
- 有且仅有一个处理器处理,随着请求结束链的滑动随之终止。
- 数据结构可以是数组 or 链表。
- 遍历方式可以是递归 or 迭代
- 链存在order顺序,可以排列优先级
- 处理器选择策略可以由"钩子"方法释放。
先看一个简单的例子,然后再继续聊
public abstract class AbstractHandler {
private AbstractHandler nextHandler;
public final Response handleMessage(Request request) {
Response response = null;
//判断是否是自己的处理级别
if (this.getHandlerLevel() == request.getBizCodeEnum()) {
response = this.echo(request);
} else {
//不属于自己的处理级别,判断是否有下一个处理者
if (this.nextHandler != null) {
response = this.nextHandler.handleMessage(request);
} else {
//没有适当的处理者,再想点办法吧
}
}
return response;
}
public void setNext(AbstractHandler _handler) {
this.nextHandler = _handler;
}
protected abstract BizCodeEnum getHandlerLevel();
protected abstract Response echo(Request request);
public enum BizCodeEnum {
A,
B
}
@Builder
@Getter
public static class Request {
//请求的等级
BizCodeEnum bizCodeEnum;
String name;
String age;
}
@Builder
@Getter
public static class Response {
boolean success;
String code;
String message;
}
}
public class HandlerA extends AbstractHandler {
@Override
protected BizCodeEnum getHandlerLevel() {
return BizCodeEnum.A;
}
@Override
protected Response echo(Request request) {
System.out.println("A");
return Response.builder().code("A").build();
}
}
public class HandlerB extends AbstractHandler {
@Override
protected BizCodeEnum getHandlerLevel() {
return BizCodeEnum.B;
}
@Override
protected Response echo(Request request) {
System.out.println("B");
return Response.builder().code("B").build();
}
}
public class Client {
public static void main(String[] args) {
//声明所有的处理节点
AbstractHandler handler1 = new HandlerA();
AbstractHandler handler2 = new HandlerB();
//设置链中的阶段顺序1-->2
handler1.setNext(handler2);
//提交请求,返回结果
Response response = handler1.handleMessage(Request.builder().bizCodeEnum(BizCodeEnum.A).build());
System.out.println(response.getCode() == "A");
Response response2 = handler1.handleMessage(Request.builder().bizCodeEnum(BizCodeEnum.B).build());
System.out.println(response2.getCode() == "B");
}
}
如上图,一个非常正经的责任链模式。
首先定义一个抽象基类AbstractHandler,将选择策略和递归的动作封装,并且暴露抽象方法定义节点的业务身份,以及节点的处理逻辑,数据结构属于链表。
从这个例子我们可以延伸一下,首先整体解耦思想和实现方式、效果上来看,比如AbstractHandler这个类,定义method的方式是一个典型的模版设计模式(final & abstract & hook method )。其次用打标的方式选择是哪个节点处理,本质上是一个策略思想。然后,如果是单节点handle这种场景,其实就相当于观察者模式变种的Pub-Sub模式,想象一样用EventBus、MQ实现的话,对不同的consumer打tag标也差不多是这个能力。但是以上几种模式的侧重点不同,只是聊起来感觉很多东西套路类似。
所以本质要解决的问题是:
- 将请求的发送和接收解耦
- 无感知的拓展接收者、并且使职责单一细化
也就是单一原则、开闭原则等。
传统责任链的拓展延伸
首先谈一下维持单节点响应设计的变种
- 单节点响应模式变种
就以上例子来说,如果我们维持只需要一个节点响应就终止链的话。可以看到,上面的例子,有几点可以玩的。
· 优先级
如果整体是一个链表遍历的话,那么我们可以在主动挂链表的时候,将优先级更靠前的往前放。比如几率比较大的节点放在前面,可以减少链表遍历的次数,有点类似平均复杂度,你可以把绝大多数碰到的情况放在第一个节点。
· 挂链
我们可以将所有节点打上@Component注解,注入Spring环境。然后通过依赖查找的方式,从Spring环境中获取List
@Component
public class Client {
private final List handlerList;
private Client(List handlerList) {
this.handlerList = handlerList;
}
public void test() {
Request request = Request.builder().bizCodeEnum(BizCodeEnum.A).build();
handlerList.stream().filter(f -> f.getHandlerLevel() == request.getBizCodeEnum())
.forEach(abstractHandler -> abstractHandler.echo(request));
}
}
细节一点的话,可以把filter提取一个方法深化语义,然后filter之后把流终止拿到一个Optional对象。
Preconditions.checkArgument(size>1,"节点不唯一");
Preconditions.checkArgument(isEmpty,"没有符合条件的节点");
以上判断,或者进行特殊逻辑,或者findAny终止判断一次之类的。
如果要保持链表数组结构也可以,在Spring ApplicationRunner等生命周期进行setNext操作即可,等于说将挂链的操作自动化处理一下,不需要我们去主动挂链了。这个就是属于spring生命周期做一些init操作范畴了,有点类似eventbus使用中,去做一些regist操作。
但是仍然是一个O(N)遍历,也可以改成用Map维护节点和业务身份的关系,通过"查表"这个老套路找到责任节点,不过这样就有点偏策略模式了。
个人觉得没必要太极端,一般来说链的节点个数本身就不会太多,实际上也基本上没有什么性能问题,责任链本身就是链的特性。
private static final Map CONTAINER =
ImmutableMap.of(
"A",new HandlerA(),
"B",new HandlerB()
);
public static void main(String[] args) {
Request request = Request.builder().bizCodeEnum(BizCodeEnum.A).build();
//提交请求,返回结果
Response response = CONTAINER.get("A").echo(request);
System.out.println(response.getCode() == "A");
}
当判断条件比较模糊的时候,还有自由组合这种场景上,责任链模式更适合。查表也只适合比较固定code这种判断。后面我们再详细看如何组合。
· 判断
上面的钩子方法属于比较明确的策略分支,也可以不使用钩子方法,用boolean变量做。
public final void handle() {
boolean handled = doHandle();
if (successor != null && !handled) { successor.handle(); }
}
protected abstract boolean doHandle();
子类去复写doHandle,只要有一个节点执行过,链就终止了。基于这种设计,我们可以复用节点,串成不同的链,在不同的场合使用。而不必强行给节点标识业务身份,链可以无状态一些,具体要看业务需求。
-
非单节点变种
在很多框架中,以及日常开发需求中,责任链一般都是所有节点都跑一遍的,或者一直跑到某个节点的终止动作。比如拦截器、函数式组合等。
这种链就比较多了,比如OkHttp、Tomcat、Rpc、Spring Interceptor等。
我们简单看几个。
我想主要介绍一下OkHttp源码中的实现,还是比较好玩的。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange)
throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.exchange != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
// Confirm that the next interceptor made its required call to chain.proceed().
if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
return response;
}
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
可以看到Okhttp中,首先是一个双向责任链,也就是执行前按顺序执行所有拦截器,然后执行后再反过来执行一遍。想要实现双向,其实有两种方案,递归或者透传对象。
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
/**
* Returns the connection the request will be executed on. This is only available in the chains
* of network interceptors; for application interceptors this is always null.
*/
@Nullable Connection connection();
Call call();
int connectTimeoutMillis();
Chain withConnectTimeout(int timeout, TimeUnit unit);
int readTimeoutMillis();
Chain withReadTimeout(int timeout, TimeUnit unit);
int writeTimeoutMillis();
Chain withWriteTimeout(int timeout, TimeUnit unit);
}
}
Okhttp的设计是用传递Chain这个接口,进行递归。每一层拦截器,都会去执行
chain.proceed();proceed内部又是通过index + 1再找下一个拦截器执行。于是整个过程就跟二叉树的前中后序遍历一样,那么在chain.proceed()之前执行的代码就是前置Interceptor逻辑,后面执行的就是后置Interceptor逻辑。
在Okhttp整体设计中,是将自定义的拦截器先加入List,后面依次追加,这就是前面那张图的执行顺序由来。
List interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
然后Interceptor也是可以提前退出链的,比如CacheInterceptor如果命中了,就不会再去走网络拦截器了,可以看到源码里return response了,彼时还没有走到proceed。
整体来说,Chain的设计比较优雅,其次双向的实现也可以借鉴,还有就是list循环用index进行推进。
Tomcat源码也是通过pos下标去跑的,定位链执行到了哪个节点。
Spring Interceptor实现的简单一些,直接平铺了几个方法。
在DispatcherServlet 的 doDispatch() 方法来分发请求,它在真正的业务逻辑执行前后,执行 HandlerExecutionChain 中的 applyPreHandle() 和 applyPostHandle() 函数,用来实现拦截的功能。
以上就是几种常见的框架责任链的玩法了。
职责链模式常用在框架的开发中,为框架提供扩展点,让框架的使用者在不修改框架源码的情况下,基于扩展点添加新的功能。
责任链与函数式编程
其实工作中很多时候遇到比较简单的场景,我们可以借由java.util.function包里面的函数式接口,结合传统的设计模式,进行简化。
像java 8推出的很多函数式接口,本身就是推荐我们使用组合等方式进行编程的,最早应该是google一些类库先推的一些api,8借鉴过来了,java也常常干这种事,比如ListenableFuture -> CompleableFuture ,LocalDate之类的。挺好的,最终java版本越来越演进,我们也可以更使用更好的范式。
比如现在有这样一个需求,在客户发起一个操作之前,我们会判断这个客户是否可以进行这个动作。比方说客户要注销某个账户,而这个账户还有未结清的账单,或者这个账户仍然有一些在途的操作,此时我们应该返回相应的提示,让客户结清账单或者取消在途订单再来注销。
我们首先定义三个Predicate
private Predicate onGoingCreditQuitPredicate() {
return creditQuit -> {
CreditQuitFindParam creditQuitFindParam = assemblerCreditQuitFindParam(creditQuit);
CreditQuitSDO existOrder = creditQuitRepository.find(creditQuitFindParam);
if (existOrder.isUnFinishCreditQuit) {
throw new CreditQuitRuleCheckException("ON_GOING_SERVICE_CLOSE_EXIST",
Lists.newArrayList("ON_GOING_SERVICE_CLOSE_EXIST"), "重复关闭请求");
}
return true;
};
}
private Predicate onGoingLoanPredicate() {
return creditQuit -> {
List existOrderList = loanRepository.findAll(assemblerLoanUserQuery(creditQuit));
Optional.ofNullable(existOrderList).ifPresent(list -> list.forEach(existOrder -> {
if (existOrder.isUnContractFinished()) {
throw new CreditQuitRuleCheckException("ON_GOING_LOAN_EXIST",
Lists.newArrayList("ON_GOING_LOAN_EXIST"), "您有未结清或已申请未放款业务,不能申请退出服务");
}
}));
return true;
};
}
private Predicate onGoingCreditPredicate() {
return creditQuit -> {
CreditFindParam creditFindParam = assemblerCreditFindParam(creditQuit);
CreditSDO existOrder = creditRepository.find(creditFindParam);
if (belongSuitableStatus(existOrder.getStatus())) {
throw new CreditQuitRuleCheckException("ON_GOING_CREDIT_EXIST",
Lists.newArrayList("ON_GOING_CREDIT_EXIST"), "在途授信流程未结束,无法进行服务关闭");
}
return true;
};
}
然后我们继续使用tmf添加应用层拓展的更多准入条件,不同的业务身份添加的自定义拓展准入条件可以不同。
@AbilityExtension(code = EXT_POINT_CHECK_RULES,
name = "服务关闭校验规则定义",
desc = "服务关闭校验规则定义")
default List> getCheckPredicates(CreditQuitSDO creditQuit) {
return Lists.newArrayList();
}
public void check(CreditQuitSDO creditQuit) {
List> checkPredicates =
Lists.newArrayList(onGoingCreditPredicate(), onGoingCreditQuitPredicate(), onGoingLoanPredicate());
CreditQuitExtPoints extension = getExtensionPoints(creditQuit.getBizCode());
List> extPredicates = extension.getCheckPredicates(creditQuit);
if (CollectionUtils.isNotEmpty(extPredicates)) {
checkPredicates.addAll(extPredicates);
}
checkPredicates.forEach(predicate -> predicate.test(creditQuit));
}
以上我们就相当于得到了小业务中台复用的准入条件,加上tmf中自定义的准入条件,结合到一起的Predicate集合,然后进行判断即可,在上层捕捉异常即可返回命中的那条失败信息,比如说onGoingCreditPredicate没有通过,就会返回用户:"在途授信流程未结束,无法进行服务关闭"。
如果不想抛异常,想提高性能,也可以返回false,然后通过别的方式透传失败话术,视场景而定,我这里性能可接受,所以选择最方便的方式上抛校验话术。
有一些场景我们也可以直接用static修饰predicate,在编译期就通过静态map编排,查表进行策略,或者switch case编排之类的。如果说在静态static predicate中有spring依赖无法引入的问题(就是static作用域引用autowared的对象,编译报错),也比较简单。
@Component
public class InstanceLocator extends SpringInitializingBean {
public static T getInstance(Class requiredType) {
return applicationContext.getBean(requiredType);
}
public static T getInstance(String name, Class requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static Map getBeansOfType(Class type) {
return applicationContext.getBeansOfType(type);
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
在static修饰的predicate中使用InstanceLocator.getBean即可,因为运行期间已经能够从spring容器中获取bean了,绕过这个限制即可。其实底层都是从DefaultListableBeanFactory获取的,ApplicationContext这个bean工厂也是通过DefaultListableBeanFactory委托管理bean的,他们是同一个接口实现类。
以上也是比较简单的玩法,当然Predicate也可以组合着玩。
比如:
onGoingCreditQuitPredicate()
.and(onGoingLoanPredicate())
.and(onGoingCreditPredicate()).test(creditQuit);
这也是一种挂链的方式。
如果我们进一步封装,可以将Predicate都放在PredicateFactory中维护,由Factory与上层交互,面向新增就在factory继续铺predicate。其实没有必要很纠结原则,比如说Predicate没有单独放一个类,设计模式是比较灵活的,在函数名和函数内部方法名很合理的情况下,可读性完全没有问题。并且有一个优势在于,多个predicate公用的方法可以提取在一个类的作用域,而私有的方法可以直接包裹在predicate内部。
比如:
private Predicate onGoingCreditPredicate() {
return new Predicate() {
@Override
public boolean test(CreditQuitSDO creditQuit) {
CreditFindParam creditFindParam = assemblerCreditFindParam(creditQuit);
CreditSDO existOrder = creditRepository.find(creditFindParam);
if (belongSuitableStatus(existOrder.getStatus())) {
throw new CreditQuitRuleCheckException("ON_GOING_CREDIT_EXIST",
Lists.newArrayList("ON_GOING_CREDIT_EXIST"), "在途授信流程未结束,无法进行服务关闭");
}
return true;
}
private boolean belongSuitableStatus(CreditStatus creditStatus) {
return !ENABLE_QUIT_STATUS.contains(creditStatus);
}
private CreditFindParam assemblerCreditFindParam(CreditQuitSDO creditQuit) {
CreditFindParam creditFindParam = new CreditFindParam();
creditFindParam.setCustomerId(creditQuit.getCustomer().getCustomerId());
return creditFindParam;
}
};
}
我们可以把belongSuitableStatus和assemblerCreditFindParam两个仅属于这个predicate私有的方法内置,然后将多个predicate公用的方法外置,来进一步细腻,可以和前面一张图对比一下,其实就已经把Predicate当一个类用了,比较简洁,替代传统设计模式的类。
回顾
责任链模式官方定义
将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。实际业务实现
可以单节点处理,也可以全部过一遍,可以数组也可以链表。Spring、Functional结合
可以通过Spring实现节点的自动注册发现、自动串联等。
可以通过Predicate等函数,进行简易的责任链实现,Predicate也是很适合作为tmf ExtendPoint定义的。
关于更多Spring和函数式小技巧单独再写一篇分享,还是有很多设计模式或者框架二次封装,可以通过Spring和Functional方式玩一些的。