java里面提供了一种内置的服务提供和发现机制,可以通过配置让一个程序在运行的时候动态加载该类的具体实现。这样子我们可以在调用某个相应接口的时候,同时达到调用某些具体类的实现功能。
具体的代码案例如下所示:
首先定义一个接口和两个接口的实现类
接口
/** * @author idea * @date 2019/5/16 */ public interface PersonAction { void say(); } 复制代码
接口实现类
/** * @author idea * @date 2019/5/16 */ public class SpiMainTest implements PersonAction { @Override public void say() { System.out.println("this is a SpiMainTest"); } } /** * @author idea * @date 2019/5/16 * @Version V1.0 */ public class SpiSubTest implements PersonAction { @Override public void say() { System.out.println("this is a SpiSubTest"); } } 复制代码
然后我们需要在META-INF/services的文件夹底下配置一份文件:
(ps:这里的配置文件命名方式为类所在包名+类名)
这份文件里面加入以下的配置信息:
(ps:文件里面输入的内容是表示类所在的地址全称,因为java的spi进行类加载的时候需要知道类所在的路径)
com.sise.dubbo.spi.SpiMainTest com.sise.dubbo.spi.SpiSubTest 复制代码
接着是编写测试类代码
import java.util.ServiceLoader; /** * @author idea * @date 2019/5/16 */ public class Demo { public static void main(String[] args) { ServiceLoader<PersonAction> serviceLoader=ServiceLoader.load(PersonAction.class); System.out.println("this is java spi"); serviceLoader.forEach(PersonAction::say); } } 复制代码
当我们执行代码之后,会发现控制台输出了相应的内容:
this is java spi this is a SpiMainTest this is a SpiSubTest 复制代码
其实jdk自带的spi功能的实现原理分为了以下几步
1.首先通过java.util.ServiceLoader来加载META-INF/services/文件夹底下的类信息
2.在运行期间需要引用相关类的时候,对加载到内存的类进行搜索和分析,进行实例化调用。
为什么是META-INF/services该文件夹呢?
在ServiceLoader类里面,我们可以通过阅读源码看到它在加载配置的时候会指定默认的加载位置META-INF/services文件夹。ServiceLoader会将该文件底下的配置类信息全部加载存储到内存中,然后在接口进行实例化的时候提供相应的实现类进行对象的实例化功能。这一点和ioc的思想有点类似,通过一个可插拔式的方式来对类的实例化进行控制。
在了解了java的spi功能之后,我们不妨再来看看dubbo的spi扩展机制。
先用一些实际的案例来进行实战的演练,然后再进行原理性的分析:
基于dubbo的spi实现自定义负载均衡算法
dubbo里面提供了一个可扩展的LoadBalance类专门供开发者们进行扩展:
/** * LoadBalance. (SPI, Singleton, ThreadSafe) * * <a href="http://en.wikipedia.org/wiki/Load_balancing_(computing)">Load-Balancing</a> * * @see com.alibaba.dubbo.rpc.cluster.Cluster#join(Directory) * @author qian.lei * @author william.liangf */ @SPI(RandomLoadBalance.NAME) public interface LoadBalance { /** * select one invoker in list. * * @param invokers invokers. * @param url refer url * @param invocation invocation. * @return selected invoker. */ @Adaptive("loadbalance") <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException; } 复制代码
这个类的头部加入了 @SPI 的注解标识,申明了该类是可以进行自定义拓展的。
在了解了loadBalance之后,我们需要在客户端加入自定义的负载均衡器代码,实现loadBalance接口
import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.cluster.LoadBalance; import java.util.List; /** * @author idea * @data 2019/5/18 */ public class MyLoadBalance implements LoadBalance { @Override public <T> Invoker<T> select(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException { System.out.println("执行自定义的负载均衡算法"); for (Invoker<T> tInvoker : list) { //可以根据url里面的相关参数做负载均衡计算 System.out.println("url: "+tInvoker.getUrl()); } //默认只请求第一台服务器 return list.get(0); } } 复制代码
这是最为基本的一种自定义负载均衡策略(永远只能请求一台机器)这种方式过于简陋,那么我们来对应用场景进行一些拓展吧,假设说现在有个需求,由于某些特定的业务常景所需,要求consumer端在9-18点之间只能请求A机器(或者说更多机器),在18-23点之间请求B机器(或者说更多机器),其余时间可以任意请求,那么这个场景下,dubbo自带的负载均衡策略
ConsistentHashLoadBalance,
RandomLoadBalance,
RoundRobinLoadBalance,
LeastActiveLoadBalance
均不支持,负载均衡该如何实现呢?
这个时候我们只能通过spi机制来自定义一套负载均衡策略进行实现了:
package com.sise.dubbo.config.loadBalanceSpi; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.cluster.LoadBalance; import java.time.LocalTime; import java.util.List; import java.util.Random; /** * * @author idea * @data 2019/5/18 */ public class MyLoadBalance implements LoadBalance { private final String A_MACHINE_HOST_PORT = "192.168.43.191:20880"; private final String B_MACHINE_HOST_PORT = "192.168.43.191:20880"; @Override public <T> Invoker<T> select(List<Invoker<T>> list, URL url, Invocation invocation) throws RpcException { System.out.println("执行自定义的负载均衡算法"); //模拟场景 System.out.println(url); int currentHour = LocalTime.now().getHour(); if (currentHour >= 9 && currentHour <= 18) { System.out.println("请求A机器"); findInvokerInList(list, A_MACHINE_HOST_PORT); } else if (currentHour >= 18 && currentHour <= 23) { System.out.println("请求B机器"); findInvokerInList(list, B_MACHINE_HOST_PORT); } int randIndex = new Random().nextInt(list.size()); return list.get(randIndex); } /** * 从服务列表里面进行dubbo服务地址匹配 * * @param list * @param matchKey * @param <T> * @return */ private <T> Invoker findInvokerInList(List<Invoker<T>> list, String matchKey) { for (Invoker tInvoker : list) { String addr = tInvoker.getUrl().getHost() + tInvoker.getUrl().getPort(); if (matchKey.equals(addr)) { return tInvoker; } } return null; } } 复制代码
然后在META-INF/dubbo文件夹底下配置一份纯文本的配置文件,文件命名为:
com.alibaba.dubbo.rpc.cluster.LoadBalance 复制代码
(ps:不同版本的dubbo,LoadBalance的包名可能不同)
在这份文件里面写入这么一行内容(有点key,value的味道)
mylb=com.sise.dubbo.config.loadBalanceSpi.MyLoadBalance 复制代码
在consumer端的配置文件中写入以下内容,这里的loadbalance需要和配置文件里的mylb一致。
<dubbo:reference interface="com.sise.dubbo.api.UserRpcService" id="userRpcService" loadbalance="mylb" /> 复制代码
然后我们可以启动多台provider,用consumer去调用这些服务进行测试,通过调整机器的时间点,控制台就会打印出不同的属性信息
请求B机器 执行自定义的负载均衡算法 zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?anyhost=true&application=consumer&check=false&dubbo=2.5.3&interface=com.sise.dubbo.api.UserRpcService&loadbalance=mylb&methods=findByUsername,findAll,printStr&pid=12460&printStr.async=true&service.filter=MyFilter&side=consumer×tamp=1558143174084&weight=1600 请求A机器 执行自定义的负载均衡算法 zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?anyhost=true&application=consumer&check=false&dubbo=2.5.3&interface=com.sise.dubbo.api.UserRpcService&loadbalance=mylb&methods=findByUsername,findAll,printStr&pid=12460&printStr.async=true&service.filter=MyFilter&side=consumer×tamp=1558143174084&weight=1600 复制代码
通过上述的这种思路,我们借助dubbo的spi机制来加载满足自己特殊业务的负载均衡器,使得该框架的灵活性更高,扩展性更强。
\
自定义的dubbo过滤器
基于spi的扩展机制,dubbo里面还提供了对于filter类型的自定义拓展。开发者可以自定义一套filter来进行对于请求的功能拦截和校验,这个有点类似于springmvc里面的filter过滤器,通过特定的过滤器拦截数据之后,可以结合特殊的业务场景来做一些控制性的功能。