系统扩展性问题是软件设计领域的一个永恒话题,也是日常开发过程中所需要考虑的核心架构问题之一。微内核架构作为解决系统扩展性问题的典型架构模式,在众多开源框架中都扮演着重要角色。本文将介绍微内核架构模式的原理、实现技术,以及如何应用到日常开发过程的方法和工程实践。
在日常开发过程中,你经常会遇到这样的需求:针对某个业务场景,我希望在系统中添加一种新的处理逻辑,但又不想对现有的系统造成太大的影响。从架构设计上讲,这是一种典型的系统扩展性需求。
针对这样的扩展性需求,本质上开发人员想要的是一种类似插件化的架构体系,调用者通过一个插件工厂获取想要的插件,而插件工厂则基于配置动态创建对应的插件,这样整体系统就像搭积木一样可以进行动态的组装。
基于插件化系统,我们往现有系统中添加新的业务逻辑时,就只需要实现一个新的插件并替换老的插件即可,系统的扩展性得到了很好的保证。那么如何实现这样的插件系统呢?微内核(MicroKernel)架构就是业界主流的实现方案。
什么是微内核架构?
微内核架构包含两部分组件,即内核系统和插件。这里的内核系统用来定义插件的实现规范,并管理着插件的生命周期。而各个插件是相互独立的组件,各自根据实现规范完成某项业务功能,并嵌入到内核系统中。
显然,基于微内核架构,当面对系统中的某个组件需要进行修改时,要做的显然只是创建一个新的组件并替换旧组件,而不需要改变原有组件的实现方式,更加不需要调整整个系统架构。
那么这里的插件具体指的是什么呢?这就需要我们引入一个概念,即SPI,英文全称叫Service Provider Interface,也就是服务提供接口。可以认为SPI就是应对系统扩展性的一个个扩展点,也是我们对系统中所应具备的扩展性的抽象。
插件化实现机制说起来简单,做起来却不容易,我们需要考虑两方面内容。一方面,我们需要梳理系统的变化并把它们抽象成SPI扩展点。另一方面,当我们实现了这些SPI扩展点之后,就需要构建一个能够支持这种可插拔机制的具体实现,从而提供一种SPI运行时环境。
接下来,我们就来一起讨论如何实现微内核架构的具体技术体系。
如何实现微内核架构?
微内核架构本质上只是为我们提供了一种架构模式,并没有规定具体的实现方式,所以原则上我们也可以提供一套满足自身要求的实现方案。但是,我们不想重复造轮子。幸好,JDK已经为我们提供了微内核架构的一种实现方式,这种实现方式针对如何设计和实现SPI提出了一些开发和配置上的规范。
对于SPI的实现者而言,具体来说有三个步骤。
对于SPI而言,我们需要设计一个服务接口,然后根据业务场景提供不同的实现类。然后在Java代码工程的META-INF/services目录中创建一个以服务接口命名的文件,并配置对应的想要使用的实现类。在代码工程中执行这些步骤,最终我们可以得到了一个包含SPI类和配置的jar包。
而对于SPI的使用者,就可以通过jar包中META-INF/services/目录下的配置文件找到具体的实现类名并进行实例化。
上图中的后面两个步骤实际上都是为了遵循JDK中SPI的实现机制而进行的配置工作。
接下来,我们可以还是通过简单的代码示例来演示这些步骤。让我们来模拟这样一个应用场景,一般业务系统中都会涉及日志组件,我们希望对业界主流的日志工具做一层包装,以便支持日志组件的灵活应用。那么,基于SPI的约定,我们将创建一个单独的工程log-spi来存放服务接口,并给出接口定义,请注意这个服务接口的完整类路径为com.spi.LogProvider,接口中只包含一个根据用户名获取密码的简单示例方法。
package com.spi;
public interface LogProvider {
//记录Info日志
public void info(String info);
}
假设系统在设计之初使用的是log4j日志库。然后我们需要实现这个服务接口,这里创建另一个代码工程log-log4j用来提供基于log4j日志库的实现,请注意,这个实现类的名称是Log4jProvider。
public class Log4jProvider implements LogProvider{
@Override
public void info(String info){
System.out.println("Log4j:” + info);
}
}
接下来的这个步骤很关键,在这个代码工程的META-INF/services/目录下需要创建一个以服务接口完整类路径命名的文件,文件的内容就是指向该接口所对应的实现类。显然,当前的这个实现类就是前面创建的Log4jProvider。
最后,我们需要创建一个外部工程log-consumer来调用服务接口,首先需要将log-log4j所生成的jar包添加到这个外部工程中的类路径中。然后,我们使用JDK中ServiceLoader工具类来完成对LogProvider实例的加载。在这里,我们通过ServiceLoader.load方法获取所有的LogProvider实例,然后遍历这些实例并调用服务接口方法。
import java.util.ServiceLoader;
import com.spi.LogProvider;
public class Main {
public static void main(String[] args) {
ServiceLoader loader = ServiceLoader.load(LogProvider.class);
for (LogProvider provider : loader) {
System.out.println(provider.getClass());
provider.info(“testInfo”);
}
}
}
运行这段代码,我们会得到系统的输出。可以看到这里获取的是针对log4j的Log4jProvider类中的具体方法实现,表示整个SPI实例的加载过程是正常的。
class com.spi.Log4jProvider
Log4j:testInfo
请注意,在上面这个Main函数中,我们并没有引入任何与Log4jProvider相关的包结构,但在运行过程中,却实现了对Log4jProvider类的动态调用,这是微内核架构的核心特征,对组件之间的依赖关系进行了解耦,从而确保了系统的扩展性。
现在,让我们来考虑这样一种场景。随着工具的更新或者架构的调整,我们需要提供一套基于logback日志库的日志实现来替换现有的基于log4j的方案。当然,我们可以重构原有代码来达成这个目标。但基于扩展性考虑,更好的办法是提供另一个SPI实例。这样,我们创建新的一个代码工程log-logback,并完成对LogProvider SPI接口的实现。请注意,这次的实现类的名称是LogbackProvider。
public class LogbackProvider implements LogProvider {
@Override
public void info(String info){
System.out.println("Logback:” + info);
}
}
同样,我们需要在这个代码工程的META-INF/services/目录下创建一个配置文件,并指向新创建的LogbackProvider类。
接下来我们把log-logback所生成的jar包也添加到外部工程的类路径中,完成这些操作之后,我们再来执行前面的main函数,得到的就是来自Log4jProvider和LogbackProvider类中的输出结果。
class com.spi.Log4jProvider
Log4j:testInfo
class com.spi.LogbackProvider
Logback:testInfo
当然,你可以根据需要提供任何LogProvider接口的实现类并动态集成到系统的执行流程中。请注意,无论是添加、替换或者移除具体的SPI实现,对于原有的log-consumer工程而言,我们并没有做任何的代码调整。这就满足了我们在开篇时提到的扩展性的设计理念,即在现有系统中添加新的组件时,不会对现有的系统造成太大的影响。
总结
作为总结,我们明确微内核架构模式是实现系统扩展性的有效手段,在Dubbo、ShardingSphere、Skywalking等主流的开源框架中都得到了广泛应用。而JDK提供的SPI机制以及ServiceLoader工具类为我们实现微内核架构提供了技术方案,帮助我们更好地把这一架构模式应用到日常开发工作中。