如何基于微内核架构模式实现系统扩展?

157 阅读8分钟

系统扩展性问题是软件设计领域的一个永恒话题,也是日常开发过程中所需要考虑的核心架构问题之一。微内核架构作为解决系统扩展性问题的典型架构模式,在众多开源框架中都扮演着重要角色。本文将介绍微内核架构模式的原理、实现技术,以及如何应用到日常开发过程的方法和工程实践。

在日常开发过程中,你经常会遇到这样的需求:针对某个业务场景,我希望在系统中添加一种新的处理逻辑,但又不想对现有的系统造成太大的影响。从架构设计上讲,这是一种典型的系统扩展性需求。

针对这样的扩展性需求,本质上开发人员想要的是一种类似插件化的架构体系,调用者通过一个插件工厂获取想要的插件,而插件工厂则基于配置动态创建对应的插件,这样整体系统就像搭积木一样可以进行动态的组装。

  WPS图片(1).png

基于插件化系统,我们往现有系统中添加新的业务逻辑时,就只需要实现一个新的插件并替换老的插件即可,系统的扩展性得到了很好的保证。那么如何实现这样的插件系统呢?微内核(MicroKernel)架构就是业界主流的实现方案。

 

什么是微内核架构?

 

微内核架构包含两部分组件,即内核系统和插件。这里的内核系统用来定义插件的实现规范,并管理着插件的生命周期。而各个插件是相互独立的组件,各自根据实现规范完成某项业务功能,并嵌入到内核系统中。

  WPS图片(2).jpeg

显然,基于微内核架构,当面对系统中的某个组件需要进行修改时,要做的显然只是创建一个新的组件并替换旧组件,而不需要改变原有组件的实现方式,更加不需要调整整个系统架构。

  WPS图片(3).jpeg

那么这里的插件具体指的是什么呢?这就需要我们引入一个概念,即SPI,英文全称叫Service Provider Interface,也就是服务提供接口。可以认为SPI就是应对系统扩展性的一个个扩展点,也是我们对系统中所应具备的扩展性的抽象。

 

插件化实现机制说起来简单,做起来却不容易,我们需要考虑两方面内容。一方面,我们需要梳理系统的变化并把它们抽象成SPI扩展点。另一方面,当我们实现了这些SPI扩展点之后,就需要构建一个能够支持这种可插拔机制的具体实现,从而提供一种SPI运行时环境。

WPS图片(4).jpeg

接下来,我们就来一起讨论如何实现微内核架构的具体技术体系。

如何实现微内核架构?

WPS图片(5).png

微内核架构本质上只是为我们提供了一种架构模式,并没有规定具体的实现方式,所以原则上我们也可以提供一套满足自身要求的实现方案。但是,我们不想重复造轮子。幸好,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。

  WPS图片(6).png

最后,我们需要创建一个外部工程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类。

  WPS图片(7).png

接下来我们把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工具类为我们实现微内核架构提供了技术方案,帮助我们更好地把这一架构模式应用到日常开发工作中。