1.Filter介绍
Dubbo引入过滤器链机制来实现功能的包装(或扩展)。Dubbo很多功能,例如泛化调用、并发控制等都是基于Filter机制实现的,系统默认的Filter在/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Filter文件中定义,内容如下:
echo=com.alibaba.dubbo.rpc.filter.EchoFilter
generic=com.alibaba.dubbo.rpc.filter.GenericFilter
genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter
token=com.alibaba.dubbo.rpc.filter.TokenFilter
accesslog=com.alibaba.dubbo.rpc.filter.AccessLogFilter
activelimit=com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
classloader=com.alibaba.dubbo.rpc.filter.ClassLoaderFilter
context=com.alibaba.dubbo.rpc.filter.ContextFilter
consumercontext=com.alibaba.dubbo.rpc.filter.ConsumerContextFilter
exception=com.alibaba.dubbo.rpc.filter.ExceptionFilter
executelimit=com.alibaba.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=com.alibaba.dubbo.rpc.filter.DeprecatedFilter
compatible=com.alibaba.dubbo.rpc.filter.CompatibleFilter
timeout=com.alibaba.dubbo.rpc.filter.TimeoutFilter
以其中一个来说明一下Filter的定义要素:
/**
* EchoInvokerFilter
*/
@Activate(group = Constants.PROVIDER, order = -110000) // @2
public class EchoFilter implements Filter { // @1
@Override
public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
if (inv.getMethodName().equals(Constants.$ECHO) && inv.getArguments() != null && inv.getArguments().length == 1)
return new RpcResult(inv.getArguments()[0]);
return invoker.invoke(inv);
}
}
代码@1:实现com.alibaba.dubbo.rpc.Filter接口。
代码@2:添加Activate,其注解含义如下:
- group: 所属组,String[],例如消费端、服务端。
- value String[],如果指定该值,只有当消费者或服务提供者URL中包含属性名为value的键值对,该过滤器才处于激活状态。
- before:String[],用于指定执行顺序,before指定的过滤器在该过滤器之前执行。
- after:string[],用于指定执行顺序,after指定的过滤器在该过滤器之后执行。
- order:用户指定顺序,值越小,越先执行。
除了支持默认的过滤器外,Dubbo还支持自定义Filter,可以通过service.filter指定过滤器,多个用英文逗号隔开,其配置方法为:
<dubbo:service ……>
<dubbo:parameter key = “service.filter” value = “filter1,filer2,…”/>
</dubbo:service>
当然,可以为所有服务提供者设置共用过滤器,其指定方法为:
<dubbo:provider …>
<dubbo:parameter key = “service.filter” value = “filter1,filer2,…”/>
</dubbo:provider>
消费端自定义过滤器的key为reference.filter,其使用方法在< dubbo:reference/>标签或< dubbo:consumer/>标签下定义属性。
2.创建分析
调用链的构建是通过下面的方法来实现:
com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.buildInvokerChain(Invoker<T>, String, String)
该方法源码如下:
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
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>() {
public Class<T> getInterface() {
return invoker.getInterface();
}
public URL getUrl() {
return invoker.getUrl();
}
public boolean isAvailable() {
return invoker.isAvailable();
}
public Result invoke(Invocation invocation) throws RpcException {
return filter.invoke(next, invocation);
}
public void destroy() {
invoker.destroy();
}
@Override
public String toString() {
return invoker.toString();
}
};
}
}
return last;
}
在构建调用链时方法先获取Filter列表,然后创建与Fitler数量一样多Invoker结点,接着将这些结点串联在一起,构成一个链表,最后将这个链的首结点返回,随后的调用中,将从首结点开始,依次调用各个结点,完成调用后沿调用链返回。这里各个Invoker结点的串联是通过与其关联的invoke方法来完成的。接下来分析这个调用链的创建。
buildInvokerChain(final Invoker<T> invoker, String key, String group)
调用buildInvokerChain时会传入invoker参数:
Invoker<T> last = invoker;
通过创建last,来指向头结点。初始状态如下图所示:
接着通过循环遍历获取到的Filter,同时创建Invoker结点,每个结点对应一个Filter。此时循环内部定义了next指针。
final Invoker<T> next = last;
该指针每次更新为指向原链表中的头结点,即last指针。 随后创建新结点,并更新last。此时新结点将作为链表中的头结点。
final Invoker<T> next = last;
last = new Invoker<T>(),
结果如下图所示。
接着通过invoke方法将各个结点串联。
public Result invoke(Invocation invocation) throws RpcException {
return filter.invoke(next, invocation);
}
在该方法内部,通过调用与该invoker关联的filter中的invoke方法来实现结点的连接。调用时将next传入invoke方法,在调用时首先会调用该结点对应的filter的invoke方法,接着调用传入参数next的invoke方法。Next的invoke方法同样会调用自己所关联的filter的invoke方法,这样就完成了结点的串联。
可以看到链表的最后一个结点就是buildInvokerChain 方法的入参invoker。最终buildInvokerChain方法通过链表头插法完成调用链的创建。因此在真正的调用请求处理前会经过若干filter进行预处理。
这里调用链的创建可以看作是职责链模式(Chain of Responsibility Pattern)的一个实现。这样系统中增加一个新的过滤器预处理请求时,无须修改原有系统的代码,只需重新建调用链即可。
3.自定义Filter
以为dubbo接口增加IP白名单为例,在开发dubbo接口时,有时可能会限制接口的访问,ip白名单即是一种。在dubbo中,通过扩展Filter接口,可以实现IP白名单的功能。
先定义一个配置IP白名单的bean:
/**
* Created by j.tommy on 2017/11/4.
*/
public class IPWhiteList {
private boolean isEnabled; // 是否启用白名单
private List<String> allowIps; // 允许的白名单列表
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public List<String> getAllowIps() {
return allowIps;
}
public void setAllowIps(List<String> allowIps) {
this.allowIps = allowIps;
}
}
然后我们实现dubbo的Filter接口:
/**
* Created by j.tommy on 2017/11/4.
*/
public class IPWhiteListFilter implements Filter{
private IPWhiteList ipWhiteList;
public void setIpWhiteList(IPWhiteList ipWhiteList) {
this.ipWhiteList = ipWhiteList;
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (!ipWhiteList.isEnabled()) {
System.out.println("dubbo IP白名单被禁用!");
return invoker.invoke(invocation);
}
String clientIp = RpcContext.getContext().getRemoteHost();
if (ipWhiteList.getAllowIps().contains(clientIp)) {
return invoker.invoke(invocation);
}
System.out.println("dubbo客户端IP[" + clientIp + "]不在白名单,禁止调用!");
return new RpcResult();
}
}
在/resources目录下,新建META-INF/dubbo目录,并新建一个名为com.alibaba.dubbo.rpc.Filter的文本文件 ,内容如下:
ipWhiteListFilter=com.tommy.service.provider.filter.IPWhiteListFilter
dubbo的配置文件中增加filter的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-provider"/>
<dubbo:registry address="multicast://224.5.6.7:1234"/>
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:provider filter="ipWhiteListFilter"/>
<dubbo:service interface="com.tommy.service.DemoService" ref="demoService"/>
<bean id="demoService" class="com.tommy.service.provider.DemoServiceImpl"/>
<bean id="ipWhiteList" class="com.tommy.service.provider.bean.IPWhiteList">
<property name="enabled" value="true"/>
<property name="allowIps">
<list>
<value>127.0.0.1</value>
<value>192.168.71.170</value>
</list>
</property>
</bean>
</beans>