案例
我们所熟悉的jbdc是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,提供了一种基准,据此可以构建更高级的工具和接口。
如上图所示,任意的一个数据库厂商只要去实现jdbc的接口,就可以轻松的对接jbdc从而为应用开发人员所服务。
SPI
上面的jdbc的设计理念叫SPI,它的全名是Service Provider Interface。它的理念是对某类功能进行抽象,确保应用程序依赖抽象而不是具体的某种实现,通过配置服务实现者的方式来达到面向接口编程以及扩展的目的。比如我们项目中需要用到日志组件,有的项目喜欢logback,有的喜欢log4j,有的喜欢common-log等等,如果在项目中直接依赖这些日志接口,那么后续如果需要对日志组件重新选型,对现有项目的影响会非常大,所有后来就有了slfj,它抽象了日志接口但不包含任何的实现,具体实现全部依赖于不同的厂商。
传统的java spi一般做法是在resources/META-INF/services/目录下面创建一个以服务接口命名的文件,该文件内容就是实现该服务接口的具体实现类的完全限定名。当程序加载的时候,就能通过resources/META-INF/services/里的配置文件找到具体的实现类名,并加载实例化。 通过这个机制就能找到服务接口的实现类,而不需要再代码里写死。
java.util.ServiceLoader这个就是java spi中用来加载服务实现类的工具,本文不对它的具体用法做过多介绍。
主题:限流策略如何扩展
本文要讨论的问题是,rpc框架中的限流过滤器扩展问题(可参考之前的文章 :简易RPC框架-客户端限流配置),之前介绍的限流实现是采用了guava提供的RateLimit,当时客户端限流的实现是在框架中写好的不允许修改,不同项目如果需要不同的限流策略那么就需要针对原有方案进行扩展,如果扩展呢?
Spring-boot 中的SPI
我在spring-boot项目中按传统的spi方式配置后,发现ServiceLoader加载指定接口找不到具体的实现类,后来发现spring-boot有自己的spi实现。它是在resources/META-INF/spring.factories中配置相关的接口,而且这个类的配置方式与传统的spi也有所不同,它采用了key=value方式,这点有点类似dubbo的spi机制。文件目录如下:
下面给出我调整之后的方案:
客户端限流接口
定义一个限流的接口,因为限流会有些参数控制,所以就增加RpcInvocation来协助完成。
public interface AccessLimitService { void acquire(RpcInvocation invocation); }
客户端限流接口实现
本文只是为了简单实现,所以直接将原有写在rpc框架中的限流方式抽取出来,并没有重新采用一种新的限流策略。
public class AccessLimitServiceImpl implements AccessLimitService { @Override public void acquire(RpcInvocation invocation) { AccessLimitManager.acquire(invocation); } static class AccessLimitManager{ private final static Object lock=new Object(); private final static Map<String,RateLimiter> rateLimiterMap= Maps.newHashMap(); public static void acquire(RpcInvocation invocation){ if(!rateLimiterMap.containsKey(invocation.getClassName())) { synchronized (lock) { if(!rateLimiterMap.containsKey(invocation.getClassName())) { final RateLimiter rateLimiter = RateLimiter.create(invocation.getMaxExecutesCount()); rateLimiterMap.put(invocation.getClassName(), rateLimiter); } } } else { RateLimiter rateLimiter=rateLimiterMap.get(invocation.getClassName()); rateLimiter.acquire(); } } } }
客户端限流过滤器调整
既然限流的实现抽取成了接口,所以此处的具体实现调整为从服务提供者中找对应的实现。
@Override public Object invoke(RpcInvoker invoker, RpcInvocation invocation) { logger.info("before acquire,"+new Date()); List<AccessLimitService> accessLimitServiceLoader = SpringFactoriesLoader.loadFactories(AccessLimitService.class, null); if(!CollectionUtils.isEmpty(accessLimitServiceLoader)){ AccessLimitService accessLimitService=accessLimitServiceLoader.get(0); accessLimitService.acquire(invocation); } Object rpcResponse=invoker.invoke(invocation); logger.info("after acquire,"+new Date()); return rpcResponse; }
目前还不支持同一个项目中多种限流策略,目前版本只允许存在一种,如果配置了多种实现,也只会选择第一个。如果需要支持也是可以的,通过配置一个名称来指定即可,但感觉价值并不大。
SpringFactoriesLoader就是spring-boot实现的类似java.util.ServiceLoader的一种服务加载工具,它负责从resources/META-INF/spring.factories中读取相应的配置,并对其加载实例化。总共包含两个核心方法:
-
loadFactoryNames
这个方法就是加载某个接口的所有指定实现类名,它可以服务于下面的loadFactories方法。
-
loadFactories 首先通过loadFactoryNames方法从配置文件中获取接口与实现类的关系,然后一个一个实例化服务实现类。
经过以上几步的调整,就基本实现了一个简单的基于SPI思想的组件扩展机制。客户端可以扩展任意的限流机制去替换。
本文源码
- RPC消费端
- RPC服务端
- RPC组件
文中代码是依赖上述项目的,如果有不明白的可下载源码