一文读懂微内核架构

什么是微内核架构?

微内核是一种典型的架构模式 ,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。微内核架构在开源框架中的应用很是普遍,好比常见的 ShardingSphere 还有Dubbo都实现了本身的微内核架构。那么,在介绍什么是微内核架构以前,咱们有必要先阐述这些开源框架会使用微内核架构的缘由。java

为何要使用微内核架构?

微内核架构本质上是为了提升系统的扩展性 。所谓扩展性,是指系统在经历不可避免的变动时所具备的灵活性,以及针对提供这样的灵活性所须要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不须要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成总体业务的升级,咱们认为这样的系统具备较好的可扩展性。mysql

就架构设计而言,扩展性是软件设计的永恒话题。而要实现系统扩展性,一种思路是提供可插拔式的机制来应对所发生的变化。当系统中现有的某个组件不知足要求时,咱们能够实现一个新的组件来替换它,而整个过程对于系统的运行而言应该是无感知的,咱们也能够根据须要随时完成这种新旧组件的替换。git


好比在 ShardingSphere 中提供的分布式主键功能,分布式主键的实现可能有不少种,而扩展性在这个点上的体现就是, 咱们可使用任意一种新的分布式主键实现来替换原有的实现,而不须要依赖分布式主键的业务代码作任何的改变 。程序员

微内核架构模式为这种实现扩展性的思路提供了架构设计上的支持,ShardingSphere 基于微内核架构实现了高度的扩展性。在介绍如何实现微内核架构以前,咱们先对微内核架构的具体组成结构和基本原理作简要的阐述。github

什么是微内核架构?

从组成结构上讲, 微内核架构包含两部分组件:内核系统和插件 。这里的内核系统一般提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各类业务代码,用来向内核系统加强或扩展额外的业务能力。在 ShardingSphere 中,前面提到的分布式主键就是插件,而 ShardingSphere 的运行时环境构成了内核系统。web

那么这里的插件具体指的是什么呢?这就须要咱们明确两个概念,一个概念就是常常在说的 API ,这是系统对外暴露的接口。而另外一个概念就是 SPI(Service Provider Interface,服务提供接口),这是插件自身所具有的扩展点。就二者的关系而言,API 面向业务开发人员,而 SPI 面向框架开发人员,二者共同构成了 ShardingSphere 自己。sql

可插拔式的实现机制提及来简单,作起来却不容易,咱们须要考虑两方面内容。一方面,咱们须要梳理系统的变化并把它们抽象成多个 SPI 扩展点。另外一方面, 当咱们实现了这些 SPI 扩展点以后,就须要构建一个可以支持这种可插拔机制的具体实现,从而提供一种 SPI 运行时环境 。数据库

如何实现微内核架构?

事实上,JDK 已经为咱们提供了一种微内核架构的实现方式,就是JDK SPI。这种实现方式针对如何设计和实现 SPI 提出了一些开发和配置上的规范,ShardingSphere、Dubbo 使用的就是这种规范,只不过在这基础上进行了加强和优化。因此要理解如何实现微内核架构,咱们不妨先看看JDK SPI 的工做原理。设计模式

JDK SPI

SPI(Service Provider Interface)主要是被框架开发人员使用的一种技术。例如,使用 Java 语言访问数据库时咱们会使用到 java.sql.Driver 接口,不一样数据库产品底层的协议不一样,提供的 java.sql.Driver 实现也不一样,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪一个数据库,在这种状况下就可使用 Java SPI 机制在实际运行过程当中,为 java.sql.Driver 接口寻找具体的实现。缓存

下面咱们经过一个简单的示例演示一下JDK SPI的使用方式:

  • 首先咱们定义一个生成id键的接口,用来模拟id生成
public interface IdGenerator {
    /**
     * 生成id
     * @return
     */

    String generateId();
}
  • 而后建立两个接口实现类,分别用来模拟uuid和序列id的生成
public class UuidGenerator implements IdGenerator {
    @Override
    public String generateId() {
        return UUID.randomUUID().toString();
    }
}

public class SequenceIdGenerator implements IdGenerator {
    private final AtomicLong atomicId = new AtomicLong(100L);
    @Override
    public String generateId() {
        long leastId = this.atomicId.incrementAndGet();
        return String.valueOf(leastId);
    }
}
  • 在项目的 resources/META-INF/services 目录下添加一个名为 com.github.jianzh5.spi.IdGenerator的文件,这是 JDK SPI 须要读取的配置文件,内容以下:
com.github.jianzh5.spi.impl.UuidGenerator
com.github.jianzh5.spi.impl.SequenceIdGenerator
  • 建立main方法,让其加载上述的配置文件,建立所有IdGenerator 接口实现的实例,并执行生成id的方法。
public class GeneratorMain {
    public static void main(String[] args) {
        ServiceLoader<IdGenerator> serviceLoader = ServiceLoader.load(IdGenerator.class);
        Iterator<IdGenerator> iterator = serviceLoader.iterator();
        while(iterator.hasNext()){
            IdGenerator generator = iterator.next();
            String id = generator.generateId();
            System.out.println(generator.getClass().getName() + "  >>id:" + id);
        }
    }
}
  • 执行结果以下:

JDK SPI 源码分析

经过上述示例,咱们能够看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,在这个方法中首先会尝试获取当前使用的 ClassLoader,而后调用 reload() 方法,调用关系以下图所示:

调用关系

在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 建立的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。以后建立 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。

public void reload() {
 providers.clear();
 lookupIterator = new LazyIterator(service, loader);
}

在前面的示例中,main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法:hasNext() 方法和 next() 方法。这里的 LazyIterator 中的 next() 方法最终调用的是其 nextService() 方法,hasNext() 方法最终调用的是 hasNextService() 方法,咱们来看看 hasNextService()方法的具体实现:

private static final String PREFIX = "META-INF/services/"
Enumeration<URL> configs = null
Iterator<String> pending = null
String nextName = null
private boolean hasNextService() {
 if (nextName != null) {
  return true;
 }
 if (configs == null) {
  try {
   //META-INF/services/com.github.jianzh5.spi.IdGenerator
   String fullName = PREFIX + service.getName();
   if (loader == null)
    configs = ClassLoader.getSystemResources(fullName);
   else
    configs = loader.getResources(fullName);
  } catch (IOException x) {
   fail(service, "Error locating configuration files", x);
  }
 }
 // 按行SPI遍历配置文件的内容 
 while ((pending == null) || !pending.hasNext()) {
  if (!configs.hasMoreElements()) {
   return false;
  }
  // 解析配置文件 
  pending = parse(service, configs.nextElement());
 }
 // 更新 nextName字段 
 nextName = pending.next();
 return true;
}

在 hasNextService() 方法中完成 SPI 配置文件的解析以后,再来看 LazyIterator.nextService() 方法,该方法「负责实例化 hasNextService() 方法读取到的实现类」,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现以下所示:

private S nextService() 
    String cn = nextName; 
    nextName = null
    // 加载 nextName字段指定的类 
    Class<?> c = Class.forName(cn, false, loader); 
    if (!service.isAssignableFrom(c)) { // 检测类型 
        fail(service, "Provider " + cn  + " not a subtype"); 
    } 
    S p = service.cast(c.newInstance()); // 建立实现类的对象 
    providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存 
    return p; 

以上就是在 main() 方法中使用的迭代器的底层实现。最后,咱们再来看一下 main() 方法中使用 ServiceLoader.iterator() 方法拿到的迭代器是如何实现的,这个迭代器是依赖 LazyIterator 实现的一个匿名内部类,核心实现以下:

public Iterator<S> iterator() 
    return new Iterator<S>() { 
        // knownProviders用来迭代providers缓存 
        Iterator<Map.Entry<String,S>> knownProviders 
            = providers.entrySet().iterator(); 
        public boolean hasNext() 
            // 先走查询缓存,缓存查询失败,再经过LazyIterator加载 
            if (knownProviders.hasNext())  
                return true
            return lookupIterator.hasNext(); 
        } 
        public S next() 
            // 先走查询缓存,缓存查询失败,再经过 LazyIterator加载 
            if (knownProviders.hasNext()) 
                return knownProviders.next().getValue(); 
            return lookupIterator.next(); 
        } 
        // 省略remove()方法 
    }; 

JDK SPI 在 JDBC 中的应用

了解了 JDK SPI 实现的原理以后,咱们再来看实践中 JDBC 是如何使用 JDK SPI 机制加载不一样数据库厂商的实现类。

JDK 中只定义了一个 java.sql.Driver 接口,具体的实现是由不一样数据库厂商来提供的。这里咱们就以 MySQL 提供的 JDBC 实现包为例进行分析。

在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,以下所示:

com.mysql.cj.jdbc.Driver

在使用 mysql-connector-java-*.jar 包链接 MySQL 数据库的时候,咱们会用到以下语句建立数据库链接:

String url = "jdbc:xxx://xxx:xxx/xxx"
Connection conn = DriverManager.getConnection(url, username, pwd); 

「DriverManager 是 JDK 提供的数据库驱动管理器」,其中的代码片断,以下所示:

static { 
    loadInitialDrivers();
    println("JDBC DriverManager initialized"); 

在调用 getConnection() 方法的时候,DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行;在 loadInitialDrivers()方法中经过 JDK SPI 扫描 Classpath 下 java.sql.Driver 接口实现类并实例化,核心实现以下所示:

private static void loadInitialDrivers() 
    String drivers = System.getProperty("jdbc.drivers"
    // 使用 JDK SPI机制加载全部 java.sql.Driver实现类 
    ServiceLoader<Driver> loadedDrivers =  
           ServiceLoader.load(Driver.class)
    Iterator<Driver> driversIterator = loadedDrivers.iterator(); 
    while(driversIterator.hasNext()) { 
        driversIterator.next(); 
    } 
    String[] driversList = drivers.split(":"); 
    for (String aDriver : driversList) { // 初始化Driver实现类 
        Class.forName(aDriver, true
            ClassLoader.getSystemClassLoader()); 
    } 

在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,一样有一段 static 静态代码块,这段代码会建立一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中(CopyOnWriteArrayList 类型),以下所示:

static { 
   java.sql.DriverManager.registerDriver(new Driver()); 
}

getConnection() 方法中,DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象建立 Connection,核心实现以下所示:

private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException 
    // 省略 try/catch代码块以及权限处理逻辑 
    for(DriverInfo aDriver : registeredDrivers) { 
        Connection con = aDriver.driver.connect(url, info); 
        return con; 
    } 

小结

本文咱们详细讲述了微内核架构的一些基本概念并经过一个示例入手,介绍了 JDK 提供的 SPI 机制的基本使用,而后深刻分析了 JDK SPI 的核心原理和底层实现,对其源码进行了深刻剖析,最后咱们以 MySQL 提供的 JDBC 实现为例,分析了 JDK SPI 在实践中的使用方式。

掌握了JDK的SPI机制就等于掌握了微内核架构的核心,以上,但愿对你有所帮助!


End




干货分享



这里为你们准备了一份小小的礼物,关注公众号,输入以下代码,便可得到百度网盘地址,无套路领取!

001:《程序员必读书籍》
002:《从无到有搭建中小型互联网公司后台服务架构与运维架构》
003:《互联网企业高并发解决方案》
004:《互联网架构教学视频》
006:《SpringBoot实现点餐系统》
007:《SpringSecurity实战视频》
008:《Hadoop实战教学视频》
009:《腾讯2019Techo开发者大会PPT》

010: 微信交流群






近期热文top



一、关于JWT Token 自动续期的解决方案

二、SpringBoot开发秘籍-事件异步处理

三、架构师之路-服务器硬件扫盲

四、基于Prometheus和Grafana的监控平台 - 环境搭建

五、RocketMQ进阶 - 事务消息



我就知道你“在看”





本文分享自微信公众号 - JAVA日知录(javadaily)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索