首页 > 其他分享 >Dubbo——Dubbo SPI解析(上)

Dubbo——Dubbo SPI解析(上)

时间:2022-10-18 17:33:20浏览次数:45  
标签:Dubbo String JDK 实现 driver SPI 解析 public

前言

Dubbo 为了更好地达到 OCP 原则(即“对扩展开放,对修改封闭”的原则),采用了“微内核+插件”的架构。那什么是微内核架构呢?微内核架构也被称为插件化架构(Plug-in Architecture),这是一种面向功能进行拆分的可扩展性架构。内核功能是比较稳定的,只负责管理插件的生命周期,不会因为系统功能的扩展而不断进行修改。功能上的扩展全部封装到插件之中,插件模块是独立存在的模块,包含特定的功能,能拓展内核系统的功能。

 

微内核架构中,内核通常采用 Factory、IoC、OSGi 等方式管理插件生命周期,Dubbo 最终决定采用 SPI 机制来加载插件,Dubbo SPI 参考 JDK 原生的 SPI 机制,进行了性能优化以及功能增强。因此,在讲解 Dubbo SPI 之前,我们有必要先来介绍一下 JDK SPI 的工作原理。

SPI简介

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样运行时可以动态的为接口替换实现类。  

它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

 

这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。

JDK SPI 机制

当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。

 

下面我们通过一个简单的示例演示下 JDK SPI 的基本使用方式:

  • 1、首先创建一个database-driver工程,并创建DataBaseDriver接口:
public interface DataBaseDriver {

    String connect(String host);
}
  • 2、创建mysql-driver工程
  • 2.1)、引入database-driver工程依赖,并实现DataBaseDriver接口:
public class MysqlDriver implements DataBaseDriver {

    @Override
    public String connect(String host) {
        return "begin build Mysql connect:"+host;
    }
}
  • 2.2)、在mysql-driver工程的 resources/META-INF/services 目录下添加一个名为 com.yibo.spi.DataBaseDriver的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:
com.yibo.spi.MysqlDriver
  • 3、创建oracle-driver工程
  • 3.1)、引入database-driver工程依赖,并实现DataBaseDriver接口:
public class OracleDriver implements DataBaseDriver{

    @Override
    public String connect(String host) {
        return "begin build Oracle connect:"+host;
    }
}
  • 3.2)、在oracle-driver工程的 resources/META-INF/services 目录下添加一个名为 com.yibo.spi.DataBaseDriver的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:
com.yibo.spi.OracleDriver
  • 4、新建client-demo工程,引入database-drivermysql-driveroracle-driver依赖
<dependencies>
	<dependency>
		<artifactId>database-driver</artifactId>
		<groupId>com.yibo</groupId>
		<version>1.0-SNAPSHOT</version>
	</dependency>

	<dependency>
		<artifactId>mysql-driver</artifactId>
		<groupId>com.yibo</groupId>
		<version>1.0-SNAPSHOT</version>
	</dependency>

	<dependency>
		<artifactId>oracle-driver</artifactId>
		<groupId>com.yibo</groupId>
		<version>1.0-SNAPSHOT</version>
	</dependency>
</dependencies>
  • 5、创建测试类,加载DataBaseDriver接口,调用connect方法
public class App {

    public static void main( String[] args ) {
        ServiceLoader<DataBaseDriver> serviceLoader = ServiceLoader.load(DataBaseDriver.class);
        System.out.println( "Java SPI" );
        for (DataBaseDriver driver : serviceLoader) {
            System.out.println(driver.connect("localhost"));
        }
    }
}

#输出
Java SPI
begin build Mysql connect:localhost
begin build Oracle connect:localhost

JDK SPI 源码分析

通过上述示例,我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,接下来我们就对其具体实现进行深入分析。

 

在 ServiceLoader.load() 方法中,首先会尝试获取当前使用的 ClassLoader(获取当前线程绑定的 ClassLoader,查找失败后使用 SystemClassLoader),然后调用 reload() 方法。

 

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

 

ServiceLoader.reload() 方法的具体实现,如下所示:

private LazyIterator lookupIterator;

// 缓存,用来缓存 ServiceLoader创建的实现对象 
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
	providers.clear();// 清空缓存 
	lookupIterator = new LazyIterator(service, loader);// 迭代器 
}

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

 

首先来看 LazyIterator.hasNextService() 方法,该方法主要负责查找 META-INF/services 目录下的 SPI 配置文件,并进行遍历,大致实现如下所示:

public final class ServiceLoader<S> implements Iterable<S>{

    private static final String PREFIX = "META-INF/services/";
	
    private class LazyIterator implements Iterator<S>{

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
				// PREFIX前缀与服务接口的名称拼接起来,就是META-INF目录下定义的SPI配 
				// 置文件(即示例中的META-INF/services/com.yibo.spi.MysqlDriver) 
                try {
                    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 集合中缓存起来,核心实现如下所示:

public final class ServiceLoader<S> implements Iterable<S>{

    private static final String PREFIX = "META-INF/services/";
	
    private class LazyIterator implements Iterator<S>{

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
		
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
				// 加载 nextName字段指定的类 
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
			// 检测类型 
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
				// 创建实现类的对象 
                S p = service.cast(c.newInstance());
				// 将实现类名称以及相应实例对象添加到缓存 
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }
	}
}

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

public final class ServiceLoader<S> implements Iterable<S>{

    private static final String PREFIX = "META-INF/services/";
	
    public Iterator<S> iterator() {
        return new Iterator<S>() {
			// knownProviders用来迭代providers缓存 
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
			// 先走查询缓存,缓存查询失败,再通过LazyIterator加载 
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
				// 先走查询缓存,缓存查询失败,再通过 LazyIterator加载 
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }
	
	private class LazyIterator implements Iterator<S>{
		。。。。。。
	}
}

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 在实践中的使用方式。

标签:Dubbo,String,JDK,实现,driver,SPI,解析,public
From: https://blog.51cto.com/u_14014612/5767859

相关文章

  • js解析json数据
      js代码constfs_port=require("fs");letrawdata=fs_port.readFileSync("Config.json");json_data=JSON.parse(rawdata);Ip_=json_data.hostport_=json_d......
  • LeetCode 54. Spiral Matrix
    ​​题目​​水题classSolution{public:vector<int>spiralOrder(vector<vector<int>>&matrix){inti=0,j=0;vector<int>ans;int......
  • UMLChina建模竞赛题答案及解析(试卷1)
    建模竞赛题比起《软件方法》书中的题目要更难一些,可以作为熟悉了《软件方法》中的基本知识之后的进一步练习。题目颇有些陷阱,应一些同学的要求,挑部分题目给出答案并详细讲解......
  • UMLChina建模竞赛题答案及解析(添加试卷2解析)
    建模竞赛题比起《软件方法》书中的题目要更难一些,可以作为熟悉了《软件方法》中的基本知识之后的进一步练习。题目颇有些陷阱,应一些同学的要求,挑部分题目给出答案并详细讲解......
  • UMLChina建模竞赛题答案及解析(添加试卷5和6解析)
    建模竞赛题比起《软件方法》书中的题目要更难一些,可以作为熟悉了《软件方法》中的基本知识之后的进一步练习。题目颇有些陷阱,应一些同学的要求,挑部分题目给出答案并详细讲解......
  • 解析目标检测全流程!附代码数据
    作者:王程伟,算法工程师在计算机视觉中,红外弱小目标检测是一个重要的方向,但直到近一两年,才开始运用一些深度学习的方法。深度学习自2012年就开始大火,为何近一两年才更多被应用......
  • python学习记录13:通用VIP视频解析播放工具源码
     importtkinterastk#导入模块importwebbrowser#视频解析主函数defshow():word=input_va.get()num=num_int_va.get()ifnum==1:link='https......
  • SpringBoot源码解析ApplicationEnvironmentPreparedEvent
    转自:https://blog.csdn.net/m0_37298252/article/details/122355631最近两篇文章主要分析了ConfigFileApplicationListener对事件ApplicationEnvironmentPreparedEvent的......
  • SpringBoot环境属性占位符解析和类型转换
    转自:https://www.cnblogs.com/throwable/p/9417827.html前提前面写过一篇关于Environment属性加载的源码分析和扩展,里面提到属性的占位符解析和类型转换是相对复杂的,这篇......
  • 面试必备:Java JUC AtomicLong 实现解析
    基于OpenJDK 12本文的目的是为后续文章解析LongAdder做一个引子,以便两者对比。AtomicPackage解析参考(比如lazySet原理解析):​​[译]JavaConcurrentAtomicPackage详解​......