来源: https://time.geekbang.org/dailylesson/detail/100075733
在面向请求 / 响应式的系统中,我们经常会在多个请求之间实现一些集中而通用处理的需求,比如要对每个请求检查数据编码方式、记录日志信息、压缩输出等。要满足这些需求,就要在请求响应代码的主流程中嵌入一些定制化组件。
所以,从架构设计上讲,为了容易添加或删除定制化组件,而不干扰主流程,我们需要在定制化组件与请求处理主流程之间实现松耦合;
同时,从提升它们的可重用性上讲,我们也需要确保每个组件都能自主存在,而且组件之间能相互独立。如下图所示:
什么是管道 - 过滤器模式?
管道 - 过滤器在结构上是一种组合行为,通常以切面(Aspect)的方式在主流程上添加定制化组件。当我们在阅读一些开源框架和代码,看到 Filter(过滤器)或 Interceptor(拦截器)等名词时,往往就是碰到了管道 - 过滤器模式。 管道 - 过滤器结构主要包括过滤器(Filter)和管道(Pipe)两种组件:管道 - 过滤器模式示例
在介绍完管道 - 过滤器模式的基本概念之后,我们来看一个它的简单示例,帮你更深入地认识这一模式。设想我们存在一个 Order 对象,代表现实世界中的订单概念,包含一些常规属性比如订单编号(orderNumber)、联系方式(contactInfo)、收货地址(address)和货物信息(goods):基于这个 Order 对象,我们构建一个 Filter 链来分别完成 Order 中核心属性的校验:
public class Order {
private String orderNumber;
private String contactInfo;
private String address;
private String goods;
}
- GoodsNotEmptyFilter,验证 Order 对象中“goods”字段是否为空;
- OrderNumberNotInvalidFilter,验证 Order 对象中“orderNumber”字段是否符合特定的命名规则;
- AddressNotInvalidFilter,验证 Order 对象中“address”字段是否是一个合法的收货地址。
管道 - 过滤器模式在开源框架中的应用
现在,相信你对管道 - 过滤器的基本结构和实现方式已经有了基本了解。接下来,我来讲解开源框架中的管道 - 过滤器设计方法和实现细节,进一步加深你的理解。 事实上,很多开源框架中都应用了管道 - 过滤器这个架构模式,也都提供了基于过滤器链的实现方式,例如 Dubbo 中的过滤器概念就基本符合我们对这一模式的理解。 接下来我们以 Dubbo 为例,先看一下过滤器链的构建过程,再介绍 Dubbo 中现有过滤器的实现方法。<font style="color:rgb(51, 51, 51);">buildInvokerChain</font>
方法:
我们可以看到用于获取扩展点的
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
//获取Invoker对象
Invoker<T> last = invoker;
//加载过滤器列表
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (filters.size() > 0) {
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
final Invoker<T> next = last;
last = new Invoker<T>() {
//讲Filter作用于Invoker对象
};
}
}
return last;
}
<font style="color:rgb(51, 51, 51);">ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension() </font>
方法。注意,这里对通过扩展点加载的过滤器进行了排序,从而确保过滤器链按设想的顺序进行执行。
看完过滤器链,我们来看一下过滤器。Dubbo 中的 Filter 接口定义是这样的:
可以看到 Filter 接口能够获取传入的 Invoker,从而对其进行拦截和处理。针对 Filter 接口,Dubbo 中一共存在一大批个实现类,类层结构如下图所示:
@SPI
public interface Filter {
Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
}
- 面向服务提供者的过滤器
- 面向服务消费者的过滤器
在代码中可以看到,通过 invoker.getUrl() 方法获取了 Invoker 中的 URL 对象,而我们知道 Dubbo 中的 URL 作为统一数据模型,它包含了所有服务调用过程中的参数,这里的 Invocation 对象则封装了请求数据。 这样,一方面我们通过 URL 对象获取本地 Token 参数,另一方面通过 Invocation 的 Attachments 也获取了 RemoteToken,可以执行对比和校验操作。这也是在 Dubbo 中处理调用信息传递的很常见的一种做法,你可以在很多地方看到类似的代码。
@Activate(group = Constants.PROVIDER, value = Constants.TOKEN_KEY)
public class TokenFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation inv)
throws RpcException {
//获取Token
String token = invoker.getUrl().getParameter(Constants.TOKEN_KEY);
if (ConfigUtils.isNotEmpty(token)) {
Class<?> serviceType = invoker.getInterface();
//获取请求的辅助信息
Map<String, String> attachments = inv.getAttachments();
//获取远程Token
String remoteToken = attachments == null ? null : attachments.get(Constants.TOKEN_KEY);
//判断本地Token和远程Token是否一致
if (!token.equals(remoteToken)) {
throw new RpcException("Invalid token! Forbid invoke remote service " + serviceType + " method " + inv.getMethodName() + "() from consumer " + RpcContext.getContext().getRemoteHost() + " to provider " + RpcContext.getContext().getLocalHost());
}
}
return invoker.invoke(inv);
}
}
总结
可以说,如何动态把握请求的处理流程是任何系统开发面临的一大问题,而今天讲解的管道 - 过滤器模式就为解决这一问题提供了有效的方案。 在日常开发过程中,我们可以在确保主流程不受影响的基础上,通过管道 - 过滤器模式添加各种定制化的附加流程,满足不同的应用场景。从架构设计上,管道 - 过滤器可以说是高内聚、低耦合思想的典型实现方案,也符合开放 - 关闭原则。各个过滤器各司其职,相互独立,多个过滤器之间集成也比较简单,在功能重用性和维护性上都具备优势。
另一方面,我们也应该认识到管道 - 过滤器的最大问题在于,导致系统形成一种成批操作行为,因此在使用过程中需要设计并协调数据的流向。