上一篇中我们介绍了一个核心接口,即SQLParserEntry,该接口位于shardingsphere-sql-parser-spi 工程的org.apache.shardingsphere.sql.parser.spi包中。而我们也提到ShardingSphere中存在多个SQLParserEntry,每个数据库都有一个SQLParserEntry实现,至于如何获取具体的SQLParserEntry,ShardingSphere采用了微内核(MicroKernel)架构,今天的主题就是讨论这个微内核架构的基本原理以及在ShardingSphere中的应用。
1. 为什么要使用微内核架构?
就架构设计而言,扩展性是软件设计的永恒话题。实现系统可扩展的方法有很多,包括回调、模块化、管道过滤器等。从实现扩展性的策略上讲,插件式系统是我们追求的一个目标。我们希望打造一种系统,调用者能够通过基于配置的插件机制动态获取它想要的任何插件。
我们知道很多编程语言具有动态加载机制。基于编程语言的动态加载机制,我们就可以实现插件化系统,在配置时而非编译时连接类。同时,通过引入工厂模式和配置化思想,我们也可以在动态加载机制上实现更为完善的自定义封装。在这里,我们关注架构模式的应用,所以我们将基于架构模式中的微内核模式来实现插件化系统。
2. 什么是微内核模式?
微内核(Microkernel)架构模式结构如下图所示,有时也被称为插件架构模式(Plug-in Architecture Pattern),通过插件向核心应用添加额外的功能,可以实现功能的独立和分离。
微内核架构包含两部分组件,即内核系统(Core system)和插件(Plug-in Component)。微内核架构的内核系统通常提供系统运行所需的最小功能集,插件是独立的组件,包含特定的处理、额外的功能和自定义代码,用来向内核系统增强或扩展额外的业务能力。
微内核是内核的一种精简形式。将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选件 这样就可提供更好的可扩展性和更加有效的应用环境。使用微内核设计,对系统进行升级,显然只要用新模块替换旧模块,不需要改变整个系统架构。
那么插件是什么?插件一般由以下几部分组成:插件暴露的接口(一般称为叫API)、插件内部实现、插件扩展点以及插件配置。其中插件扩展点我们一般设计为SPI(Service Provider Interface,服务提供接口)。
微内核模式的本质是管理插件以及协调插件之间的调用。插件插件本身是一个很大粒度的扩展点,可以整个被替换。同时插件可以提供自己的小粒度扩展点。这样整个系统就是由一个微内核加很多插件组成一个具备很强扩展性的系统。
3. 微内核模式的基本实现:Java SPI机制
微内核架构应用广泛,在流行的分布式服务框架Dubbo中,涉及到通信框架Mina、Netty和Grizzly,序列化方式Hession、JSON,传输协议Dubbo、RMI等都是这一架构风格的体现。而在ShardingSphere中,上一篇中提到的用于SQL解析的SQLParserEntry,以及本系列后面会介绍的用于生成分布式主键的ShardingKeyGenerator、用于数据脱敏的ShardingEncryptor、用于分布式事务的ShardingTransactionManager以及用于数据库治理的注册中心接口RegistryCenter都用到了微内核架构。
基于微内核架构,我们可以通过简单的配置就能对这些具体实现进行排列组合构成丰富的运行时环境。微内核架构风格提供的是一种解决扩展性问题的思路,而JDK也提供了这一思路的实现方案,即SPI(Service Provider Interface)机制。
SPI是提供给服务提供商与扩展框架功能的开发者使用的接口,如果系统需要提供新的API实现并打包代码,那么通过SPI机制我们就可以在不修改JAR包或框架的时候为该API提供新实现,这就是SPI所面向的主要应用场景,也是插件化所需要解决的核心问题。
SPI约定在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为API具体实现类的全限定名。当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录同时创建一个以服务接口命名的文件,该文件里配置着实现该服务接口的具体实现类,而该具体实现类必须有一个不带参数的构造方法。SPI机制的结构如下图所示。
而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里硬编码指定。JDK提供了服务实现查找的一个工具类java.util.ServiceLoader来实现SPI机制。
接下来通过一个示例来演示基于Java SPI的实现过程。该示例非常简单,我们先从一个IHello接口开始:
public interface IHello {
void sayHello();
}
然后我们针对该接口,提供两个简单的实现类,分别是Hello1和Hello2,如下所示。
public class Hello1 implements IHello{
@Override
public void sayHello(){
System.err.println("hello world---1");
}
}
public class Hello2 implements IHello{
@Override
public void sayHello(){
System.err.println("hello world---2");
}
}
这些代码都很简单,我们把它们放在一个名为SPIProvider的工程中,假设该工程的类路径为com.tianyalan.spi。作为服务的提供者,这时候我们就需要在META-INF/services/文件夹里提供一个服务定义文件,基于类路径规则,该文件只能被命名为com.tianyalan.spi.IHello。作为演示,我们先在该文件中写入前面定义的Hello1的完整类路径,即com.tianyalan.spi.Hello1。然后,我们把该工程导出为一个jar包备用。
接下来要做的事情是新建另一个代码工程,该代码工程需要引用前文中所生成的jar包,并完成如下所示的Main函数。
public class Main {
public static void main(String[] args) {
ServiceLoader<IHello> loader = ServiceLoader.load(IHello.class);
for (IHello hello : loader) {
System.out.println(hello.getClass());
hello.sayHello();
}
}
}
现在,该工程的角色是SPI服务的使用者,这里使用了JDK提供的ServiceLoader工具类来获取所有IHello的实现类。我们知道现在在jar包的META-INF/services/com.tianyalan.spi.IHello文件只有一个com.tianyalan.spi.Hello1类的定义,所以这时候我们只能获取Hello1的实现。执行这段Main函数,我们将得到是输出为:
class com.test.Hello1
hello world---1
接下来我们调整META-INF/services/com.tianyalan.spi.IHello文件中的内容,写入com.tianyalan.spi.Hello1和com.tianyalan.spi.Hello2,并重新打成jar包供SPI服务的使用者进行引用。再次执行Main函数,则可以得到一下输出:
class com.test.Hello1
hello world---1
class com.test.Hello2
hello world---2
至此,完成的SPI提供者和使用者的实现过程演示完毕。这个示例非常简单,但却是ShardingSphere中实现微内核架构的基础。接下来,就让我们把话题转换到ShardingSphere,看看ShardingSphere中应用SPI机制的具体方法。这是下一篇我们要讨论的内容。
更多内容可以关注我的公众号:程序员向架构师转型。
天涯兰的博客 博客专家 发布了93 篇原创文章 · 获赞 9 · 访问量 11万+ 私信 关注