共计 10095 个字符,预计需要花费 26 分钟才能阅读完成。
导读:
需要变动是程序员生命中惟一不变的事件,本文将介绍 JDK/Spring/Dubbo 中的 SPI 机制,以此来帮忙咱们编写出一套可扩展性强,易于保护的代码框架。
文|杨亮 网易云商高级 Java 开发工程师
一、什么是 SPI?
SPI(Service Provider Interface)是一种旨在由第三方实现或者扩大的 API。它能够用于启用、扩大甚至替换框架中的组件。 SPI 的目标是为了在不批改原来的代码库的根底上,开发人员能够应用新的插件或者模块来加强框架性能。如咱们常应用的 JDBC,在 Java 的外围类库中,并没有规定开发者须要应用何种类型的数据库,开发者能够依据本身需要来抉择不同的数据库类型,能够是 MySQL、Oracle。
所以 Java 的外围类库只提供了数据库驱动的接口 Java.sql.Driver,不同的数据库服务提供商能够实现此接口,而开发者只需配置相应数据库驱动的实现类,JDBC 框架就能自行加载第三方的服务以达到客户端拜访不同类型的数据库的性能。
在很多支流的开发框架中,咱们都能够看到 SPI 的身影,除了 JDK 提供的 SPI 机制外,还有诸如 Spring、Spring cloud Alibaba Dubbo 等等,接下来笔者将介绍如何应用它们及其实现原理。
二、JDK SPI
(一)案例
- 定义接口标准
package com.demo.jdkspi.api;public interface SayHelloService {String sayHello(String name);}
- 定义接口实现类
public class SayHelloImpl implements SayHelloService {public String sayHello(String name) {return "你好"+name+", 欢送关注网易云商!";}}
-
配置文件
在 resources 目录下增加纯文本文件 META-INF/services/com.demo.jdkspi.api.SayHelloService,内容如下:
com.demo.jdkspi.impl.SayHelloServiceImpl
-
编写测试类
客户端引入依赖,并应用 ServiceLoader 加载接口:
public static void main(String[] args) {// 1. 依据 SayHelloService.class 创立 ServiceLoader 实例,此时 SayHelloService 实例并没有被创立(懒加载)ServiceLoader<SayHelloService> loader = ServiceLoader.load(SayHelloService.class); // 2. SayHelloService 实例是在遍历的时候创立的 loader.forEach(sayHelloService ->{ System.out.println(sayHelloService.sayHello("Jack")); });}
运行后果如下:
(二)JDK SPI 原理解析
通过案例咱们能够晓得 JDK SPI 机制次要是通过 ServiceLoader 来实现的, 须要留神的是,实现类的加载是一种懒加载机制,创立 ServiceLoader 并不会去加载接口实现,而是在遍历的时候再去加载。
创立 ServiceLoader 实例流程:
次要流程形容
- 获取线程上下文的 ClassLoader: 因为 ServiceLoader 是在 rt.jar 下的,而接口实现类是在 classpath 上面,这突破了双亲委派模型,所以须要从线程上下文中获取 AppClassLoader 用于加载指标接口及其实现类。
- 清空 providers 缓存: 清空历史加载缓存。
- 创立 LazyIterator,后续遍历所有实现类的时候会应用此迭代器。
加载指标服务流程:
次要流程形容
- 在迭代器开始遍历前,SayHelloService 会去加载 ClassPath(由前文提到的 AppClassLoader 决定的)下所有的指标接口的配置信息。
- 接口实现类的实例化 次要是先通过 Class.forName 创立一个 Class 对象,而后通过反射创立实例。
- 在实现类实例化后,ServiceLoader 会依据实现类的全限定名为标识将实例缓存起来。
(三)JDK SPI 总结
长处:
- 解耦: JDK SPI 使得第三方服务模块加载管制的逻辑与调用者的业务代码拆散,从而实现解耦。
- 懒加载: 在创立 ServiceLoader 实例的时候并不会去加载第三方服务模块,而是在遍历的时候去加载。
毛病
- 只能通过遍历的形式去获取所有的接口实现类,并没有实现按需加载。
- 如果接口实现类依赖了其余扩大实现,JDK SPI 并没有实现依赖注入的性能。
三、Spring SPI
Spring Boot Starter 是一种依赖的汇合,它使得咱们只须要进行简略的配置就能获取 Spring 和相干技术的一站式服务。而 Spring Boot Starter 的实现也离不开 SPI 思维,上面咱们通过实现一个简略的 starter 组件来领会一下它的魅力。
(一)Spring Boot Starter 案例
-
编写 SayHello Service 的实现类及 Spring 配置类
创立一个独立的我的项目 greeter-spring-boot-starter,并编写 SayHelloService 实现类及 Spring 配置类
public class Greeter implements SayHelloService, InitializingBean {public String sayHello(String name) {return "你好"+name+", 欢送关注网易云商!";} public void afterPropertiesSet() throws Exception { System.out.println("网易云商服务加载结束,欢送应用!"); }}
@Configurationpublic class TestAutoConfiguration {@Bean public SayHelloService sayHelloService(){return new Greeter(); }}
-
配置文件
在 resources/META-INF 目录下创立 spring.factories 文件,内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.demo.springspi.TestAutoConfiguration
-
引入依赖
在客户端我的项目中援用 greeter-spring-boot-starter 依赖
<dependency> <groupId>com.spi.demo</groupId> <artifactId>greeter-spring-boot-starter</artifactId> <version>1.0.0-SNAPSHOT</version></dependency>
-
成果展现
在客户端 Spring 我的项目启动的时候,能够分明的看见,咱们编写的 Greeter 会被 Spring IoC 容器加载。
(二)Spring Boot Starter 原理解析
在 Spring SPI 中,也有一个相似于 ServiceLoader 的类——SpringFactoriesLoader,在 Spring 容器启动的时候,会通过 SpringFactoriesLoader 去“META-INF/spring.factories”获取配置类信息,而后将这些配置类信息封装成 BeanDefinition,这样 Spring IoC 容器就能治理这些 Bean 了,次要流程如下:
次要流程形容:
- SpringFactoriesLoader 加载配置类信息产生在构建 SpringApplication 实例的时候,SpringFactoriesLoader 会读取“META-INF/spring.factories”下的配置信息并缓存起来。
- AutoConfigurationImportSelector 是在 @EnableAutoConfiguration 中引入的,AutoConfigurationImportSelector 的外围性能是:获取 “org.springframework.boot.autoconfigure.EnableAutoConfiguration” 的配置类列表,并且会筛选一遍(如咱们在 @EnableAutoConfiguration 中配置了 exclude 属性),失去最终须要加载的配置类列表。
- ConfigurationClassPostProcessor 会将最终须要加载的配置类列表并将其加载为 BeanDefinition,后续在解析 BeanClass 的时候,也会调用 Class.forName 来获取配置类的 Class 对象。Spring Bean 的装载流程本文不再赘述。
(三)Spring SPI 总结
- 通过将第三方服务实现类交给 Spring 容器治理,很好解决了 JDK SPI 没有实现依赖注入的问题。
- 配合 Spring Boot 条件拆卸,能够在肯定条件下实现按需加载第三方服务,而不是加载所有的扩大点实现。
四、Dubbo SPI
SPI 机制在 Dubbo 中也有所利用,Dubbo 通过 SPI 机制加载所有的组件,只不过 Dubbo 并未应用 Java 原生的 SPI 机制,而是对其进行了加强。在 Dubbo 源码中,常常能看到如下代码,它们别离是指定名称扩大点,激活扩大点和自适应扩大点:
ExtensionLoader.getExtensionLoader(XXX.class).getExtension(name);ExtensionLoader.getExtensionLoader(XXX.class).getActivateExtension();ExtensionLoader.getExtensionLoader(XXX.class).getAdaptiveExtension(url,key);
Dubbo SPI 的相干逻辑都封装在了 ExtensionLoader 类中,通过 ExtensionLoader 咱们能够加载指定的实现类,Dubbo 的 SPI 扩大有两个规定:
- 须要在 resources 目录下创立任意目录构造: META-INF/dubbo、META-INF/dubbo/internal、META-INF/services 在对应的目录下创立以接口全路径名命名的文件。
- 文件内容是 Key 和 Value 模式的数据, Key 是一个字符串,Value 是一个对应扩大点的实现。
(一)指定名称扩大点
案例
-
申明扩大点接口
在一个依赖了 Dubbo 框架的工程中,创立一个扩大点接口及一个实现,扩大点接口须要应用 @SPI 注解,代码如下:
@SPIpublic interface SayHelloService {String sayHello(String name);}
public class SayHelloServiceImpl implements SayHelloService {@Override public String sayHello(String name) {return "你好"+name+", 欢送关注网易云商!";}}
-
配置文件
在 resources 目录下增加纯文本文件 META-INF/dubbo/com.spi.api.dubbo.SayHelloService,内容如下:
neteaseSayHelloService=com.spi.impl.dubbo.SayHelloServiceImpl
- 编写测试类
public static void main(String[] args) {ExtensionLoader<SayHelloService> extensionLoader = ExtensionLoader.getExtensionLoader(SayHelloService.class); SayHelloService sayHelloService = extensionLoader.getExtension("neteaseSayHelloService"); System.out.println(sayHelloService.sayHello("Jack"));}
(二)激活扩大点
有些时候一个扩大点可能有多个实现,咱们心愿获取其中的某一些实现类来实现简单的性能,Dubbo 为咱们定义了 @Activate 注解来标注实现类,表明该扩大点为激活扩大点。其中 Dubbo Filter 是咱们平时罕用的激活扩大点。
案例
在服务提供者端实现两个性能,一个是在服务调用的时候打印调用日志,第二个是查看零碎状态,如果零碎未就绪,则间接返回报错。
- 定义打印日志的 filter
/** * group = {Constants.PROVIDER}示意在服务提供者端失效 * order 示意执行程序,越小越先执行 */@Activate(group = {Constants.PROVIDER}, order = Integer.MIN_VALUE)public class LogFilter implements Filter {@Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {System.out.println("打印调用日志"); return invoker.invoke(invocation); }}
- 定义零碎状态查看的 filter
@Activate(group = {Constants.PROVIDER},order = 0)public class SystemStatusCheckFilter implements Filter {@Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {// 校验零碎状态,如果零碎未就绪则调用失败 if(!sysEnable()) {throw new RuntimeException("零碎未就绪,请稍后再试"); } System.out.println("零碎准备就绪,能失常应用"); Result result = invoker.invoke(invocation); return result; }}
-
配置文件
在 resources 目录下增加纯文本文件 META-INF/dubbo/com.alibaba.dubbo.rpc.Filter,内容如下:
logFilter=com.springboot.dubbo.springbootdubbosampleprovider.filter.LogFiltersystemStatusCheckFilter=com.springboot.dubbo.springbootdubbosampleprovider.filter.SystemStatusCheckFilter
-
执行成果
在服务提供者端,执行指标办法之前,会先去执行咱们定义的两个 Filter,成果如图所示:
(三)自适应扩大点
自适应扩大点就是能依据上下文动静匹配一个扩大类,有时候有些扩大并不想在框架启动阶段被加载,而是心愿在扩大办法被调用时,依据运行时参数进行加载。
案例
- 定义自适应扩大点接口
@SPI("default")public interface SimpleAdaptiveExt {/** * serviceKey 示意会依据 URL 参数中 serviceKey 的值来寻找对应的扩大点实现,* 如果没有找到就应用默认的扩大点。*/ @Adaptive("serviceKey") void sayHello(URL url, String name);}
- 定义扩大点实现类
public class DefaultExtImp implements SimpleAdaptiveExt {@Override public void sayHello(URL url, String name) {System.out.println("Hello" + name); }}
public class OtherExtImp implements SimpleAdaptiveExt {@Override public void sayHello(URL url, String name) {System.out.println("Hi" + name); }}
-
配置文件
在 resources 目录下增加纯文本文件 META-INF/dubbo/com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt,内容如下:
default=com.spi.impl.dubbo.adaptive.DefaultExtImpother=com.spi.impl.dubbo.adaptive.OtherExtImp
- 编写测试类
public static void main(String[] args) {SimpleAdaptiveExt simpleExt = ExtensionLoader.getExtensionLoader(SimpleAdaptiveExt.class).getAdaptiveExtension(); Map<String, String> map = new HashMap<String, String>(); URL url = new URL("http", "127.0.0.1", 1010, "path", map); // 调用默认扩大点 DefaultExtImp.sayHello 办法 simpleExt.sayHello(url, "Jack"); url = url.addParameter("serviceKey", "other"); // 此时 serviceKey=other,会调用扩大点 OtherExtImp.sayHello 办法 simpleExt.sayHello(url, "Tom");}
(四)Dubbo 扩大点原理剖析
获取 ExtensionLoader 实例
ExtensionLoader.getExtensionLoader 这个办法次要返回一个 ExtensionLoader 实例,次要逻辑如下:
- 先从缓存“EXTENSION_LOADERS”中获取扩大类对应的实例;
- 如果缓存未命中,则创立一个新的实例,保留在 EXTENSION_LOADERS 中;
- 在 ExtensionLoader 构造方法中,会初始化一个 ExtensionFactory;
获取扩大点办法 getExtension
- 先从缓存 cachedClasses 中获取扩大类,如果没有就从 META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/services/ 三个目录中加载。
- 获取到扩大类当前,查看缓存 EXTENSION_INSTANCES 中是否有该扩大类的实现,如果没有就通过反射实例化后放入缓存中。
- 实现依赖注入,如果以后实例依赖了其余扩大实现,那么 Dubbo 会将依赖注入到以后实例中。
- 将扩大类实例通过 Wrapper 装璜器进行包装。
以上步骤中,第一个步骤是加载扩大类的要害,第三和第四个步骤是 Dubbo IoC 与 AOP 的具体实现。其中依赖注入是通过调用 injectExtension 来实现的且只反对 setter 形式的注入。
获取自适应扩大点办法 getAdaptiveExtension
- 调用 getAdaptiveExtensionClass 办法获取自适应扩大 Class 对象。
- 通过反射进行实例化。调用 injectExtension 办法向扩大类实例中注入依赖。
尽管上述三个流程和和一般扩大点的获取办法相似,然而在解决 Class 对象的时候,Dubbo 会动静生成自适应扩大点的动静代理类,而后应用 javassist(默认)编译源码,失去代理类 Class 实例。其中动静生成的自适应扩大类的源码如下(以上述代码中的 SimpleAdaptiveExt 为例):
package com.spi.impl.dubbo.adaptive;import org.apache.dubbo.common.extension.ExtensionLoader;public class SimpleAdaptiveExt$Adaptive implements com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt {public void sayHello(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("serviceKey", "default"); if(extName == null) throw new IllegalStateException("Failed to get extension (com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt) name from url (" + url.toString() + ") use keys([serviceKey])"); com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt extension = (com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt)ExtensionLoader.getExtensionLoader(com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt.class).getExtension(extName); extension.sayHello(arg0, arg1); }}
从上述代码中咱们能够看到,在办法 SayHello 中,会去获取 url 中 serviceKey 对应的值,如果有就应用该值对应的扩大点实现,否则应用默认的扩大点实现。
(五)Dubbo SPI 总结
Dubbo 的扩大点加载从 JDK SPI 扩大点发现机制增强而来,并且改良了 JDK SPI 的以下问题:
- JDK SPI 会一次性实例化扩大点所有实现,而 Dubbo 能够应用自适应扩大点,在扩大办法调用的时候再实例化。
- 减少了对 IoC 的反对,一个扩大点能够通过 setter 形式来注入其余扩大点。
- 减少了 AOP 的反对,基于 Wrapper 包装器类来加强原有扩大类实例。
五、多租户零碎中定制技术联合 SPI 瞻望
多租户零碎中动静个性化配置与定制技术能满足不同租户的个性化要求,然而大量的定制工作可能使零碎变得十分复杂。
为了方便管理及保护不同租户的个性化配置,联合 SPI 能够应用不同扩大实现来启用或扩大框架中的组件的思维,咱们能够设计一个租户个性化定制治理平台,该平台能治理各个租户的定制化配置, 开发人员将不同租户的个性化差别形象为一个个的定制点,定制治理平台能收集并治理这些定制点信息,业务零碎在运行时能从定制平台中获取租户的个性化配置并加载相应的扩大实现,从而满足不同租户的个性化需要。整体架构如下:
租户个性化定制治理平台次要性能及个性如下:
- 形象定制点: 开发人员将租户特色形象成不同的定制点接口,对于不同特色的租户有不同的扩大实现。
- 定制点发现: 每个服务的定制点及实现信息须要上报给定制治理平台。
- 定制租户个性化配置: 经营人员能够依据租户的特色配置不同的定制点实现。
- 动静加载: 在租户拜访业务零碎的具体服务时,业务零碎能从治理平台中获取到相应租户的配置信息,并且能够通过责任链 / 装璜器模式来组装一个或者多个定制点实现。
- 租户隔离: 经营人员为租户设置好个性化配置后,定制治理平台可能将配置信息以租户的维度存储,从而实现不同租户定制内容的隔离。
- 定制复用: 对租户共有特色进行重用配置或者对那些没有配置的租户采纳默认配置。
租户个性化定制治理平台能够将租户个性化特色以元数据的形式进行治理, 后续只有新租户的个性化需要能通过现有定制点的元数据进行形容,那么只须要批改配置的形式来满足新需要,即便满足不了,也只须要新增或者实现定制点接口并且上报给定制治理平台,这使得零碎易于保护,代码复用性也会更高。
参考资料
《Dubbo 2.7 开发指南》
《Spring Cloud Alibaba 微服务原理与实战》
作者介绍
杨亮,网易云商高级 Java 开发工程师,负责云商平台公共业务模块和外部中间件的设计与开发。