SPI简介及源码简析

SPI简介及源码简析

一,背景介绍

数据库连接驱动JDBC大家都知道,我们在初学JAVA的时候,加载数据库驱动的方式一定是:Class.forName(“com.mysql.jdbc.Driver”)。但是后来慢慢不这么写了,甚至于在后来的项目中,都已经不关注这它了,只要把mysql或者oracle的驱动放在maven文件的依赖中,就可以正常的加载到数据的连接驱动。项目可以正常运行了,随着紧凑的项目进度安排,可能后续因为时间紧任务重,也就不会再去关注它了。

但是我们是什么时候开始,不再去写Class.forName这样的代码了呢?不写的话,数据库驱动又是怎么被加载的呢?

这个分界点,准确来说,是JDK6的发布。

原来,在JDK6的时候,新增了一种特性,叫SPI机制。随着JDK6一起发布的,还有JDBC4.0。

同时,JDBC4.0也随着SPI机制的引入,增加了一个 *auto-loading of JDBC driver class*的特性。数据库驱动就是通过这个特性实现自动加载的。是它们,解放了我们的双手,不用再写那些重复的代码。

在JDBC4.0的API特性介绍中有这么一句话:

在这里插入图片描述

意思是:支持驱动类的自动加载。接来重点是:JAR包里的META-INF/services文件夹,名为java.sql.Driver的文件,如果这个文件包含驱动类的名字,那JDBC驱动就支持自动加载。

疑问三连:

1,那这个自动加载JDBC驱动类的特性,又是怎么实现自动加载的呢?它和SPI机制有什么内在的联系吗?

2,这种自动加载的方式是否抛弃了Class.forName(),采用了新的方式呢?

3,具体又是怎么做的呢?

回答这些问题之前,我们先来了解一下SPI。

二,SPI概念

SPI是Service Provider Interface 的简称,字面翻译是服务提供者接口,但其实是一个服务发现机制。SPI是一种扩展机制,在对应的配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化,实现和接口不在一起。接口在调用方,而实现在其他地方,通过SPI机制,保证在程序运行时,动态为接口替换实现类。SPI机制可以为某个接口寻找服务实现。

有一个与它很像的名字,叫API,应用程序接口。但是二者并非同级同类,不可并论。SPI更像组织API的一种方式。如果接口和实现在同一个包,这是我们通常理解下的API,我们引用的非JDK包含的一些第三方工具类,如okhttp3,也都是这么做的。

那如果接口和实现分别属于独立的包,调用的时候,只需要知道接口就好,这样的接口和实现,本质上还是属于API。而这种情况下,把它们连接到一起,在程序运行时期二者就像普通的API一样对外提供服务的这个中间人,就是SPI,可以把SPI理解是连接接口规范和实现的桥梁。

而接口规范和实现分离的情况,在JDK中尤为常见。JDK定义了很多的接口规范,例如JDBC,servlet,javax.validation,日志等等。而我们日常使用的时候,只需要知道JDBC等这些接口就好了,而不用去关心它们是由谁实现的,在哪儿。而这些接口和实现的组合本质上还是属于API。

在这里插入图片描述

三,原理解析

概念都比较抽象,来点实际的吧。

先说回答问题:

1,那这个自动加载JDBC驱动类的特性,是通过JDK6发布的SPI机制实现了自动加载。

2,这种自动加载的方式的本质依旧是通过Class.forName()反射获取驱动实现类信息,进而实例化驱动实现类。

3,数据库驱动实现类厂商,只需要在驱动类的包中新建:META-INF/services包,在包下新建文件,命名为:java.sql.Driver,这样项目在引入对应的驱动的maven依赖的时候,就可以通过自动加载的方式加载到驱动了。

接下来,我们来验证一下,并且去弄清楚原理。

1,验证

DriverManager中源码如下:

//一下代码摘自java.sql.DriverManager#ensureDriversInitialized
public Void run() {
    //通过ServiceLoader加载
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    while (driversIterator.hasNext()) {
        driversIterator.next();
}

手动模拟:

1  ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
2  Iterator<Driver> iterator = load.iterator();
3  while (iterator.hasNext()){
4      Driver next = iterator.next();
5      System.out.println("已加载的驱动:"+next.getClass());
6      System.out.println();
7  }

运行结果:

已加载的驱动:class com.mysql.jdbc.Driver

已加载的驱动:class com.mysql.fabric.jdbc.FabricMySQLDriver

可以看到我们目前的应用应用在启动的时候,加载了两个Mysql的驱动。其余的驱动还有Oracle,H2等。

我们打开在mysql驱动的jar发现,符合自动加载的要求,反证成立。

在这里插入图片描述

2,分析

ServiceLoader的功能比较齐全,比如通过模块服务加载,通过类路径下的配置文件加载,通过制定类加载器的模块服务加载等诸多方式。本文只分析通过类路径下配置文件加载的方式,即Driver.class的实现类加载的过程,并且Driver.class的实现类仅限于com.mysql.jdbc.Driver,和com.mysql.fabric.jdbc.FabricMySQLDriver这两种。其他的驱动不做分析,原理是一样的。

主要流程如下:

在这里插入图片描述

注:与类路径懒加载的迭代器(LazyClassPathLookupIterator)同级的还有模块服务加载迭代器(ModuleServicesLookupIterator)迭代器,详情请见:https://zhuanlan.zhihu.com/p/30860041

本文只分析类路径懒加载的迭代器。

根据流程,截取的ServiceLoader部分关键代码:

//1,传入Driver.class
//2,获取当前线程的类加载器
//3,构造ServiceLoader实例
@CallerSensitive
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }
//4,构造Iterator实例
//5,遍历Iterator
//11,并将最后实例化之后的实现类缓存起来
public Iterator<S> iterator() {

        lookupIterator1 = newLookupIterator();

        return new Iterator<S>() {
            @Override
            public boolean hasNext() {
                return lookupIterator1.hasNext();
            }
            
            @Override
            public S next() {
                next = lookupIterator1.next().get();
                instantiatedProviders.add(next);
                return next;
            }
        };
   }

//6,构造类路径懒加载Iterator
private Iterator<Provider<S>> newLookupIterator() {
Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
    return new Iterator<Provider<S>>() {
        @Override
        public boolean hasNext() {
            return (second.hasNext());
        }
        @Override
        public Provider<S> next() {
            (second.hasNext()) {
                return second.next();
            } else{
                throw new NoSuchElementException();
            }
        }
    };
}
//7,遍历类路径懒加载Iterator
 private final class LazyClassPathLookupIterator<T>
            implements Iterator<Provider<T>> {
        @Override
        public boolean hasNext() {
            return hasNextService();
        }

        @Override
        public Provider<T> next() {
            return nextService();
        }
		//10,获取实现类信息之后,再获取实现类的构造器
        private boolean hasNextService() {
            Class<?> clazz = nextProviderClass();
            Class<? extends S> type = (Class<? extends S>) clazz;
            Constructor<? extends S> ctor
                    = (Constructor<? extends S>) getConstructor(clazz);
            ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
            nextProvider = (ProviderImpl<T>) p;
        }
		//8,拼接全路径 fullName=META-INF/services/java.sql.Driver
        //9,加载配置文件
     	//10,通过Class.forName()反射获取实现类信息
        private Class<?> nextProviderClass() {
            static final String PREFIX = "META-INF/services/"
            String fullName = PREFIX + service.getName();
            configs = loader.getResources(fullName);
            pending = parse(configs.nextElement());
            return Class.forName(cn, false, loader);
        }

        private Provider<T> nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            Provider<T> provider = nextProvider;
            return provider;
        }
    }
private static class ProviderImpl<S> implements Provider<S> {
    //11,实例化实现类
    @Override
    public S get() {
                return newInstance();
            }
        }
    //11,实例化实现类
	 private S newInstance() {
        S p = null;
        p = ctor.newInstance();
        }
    }

备注:

在ServiceLoader的属性中,有如下3个:

// The lazy-lookup iterator for iterator operations
private Iterator<Provider<S>> lookupIterator1;
private final List<S> instantiatedProviders = new ArrayList<>();

// The lazy-lookup iterator for stream operations
private Iterator<Provider<S>> lookupIterator2;

1,实例化之后,将实例放在 List<S> instantiatedProviders中缓存,这个改动是在JDK9时加进去的。

2,属性中有两个Iterator<Provider<S>>,lookupIterator1是我们以上分析iterator()用到的。lookupIterator2是stream()方法用到的。stream()方法的好处在于,可以不用实例化接口实现,对接口的实现进行一些类型检查等操作(通过java.util.ServiceLoader.Provider#type方法)。

四,“逆向”的双亲委派机制

细心的读者可能会有疑问,Driver.classs是位于rt包下,而我们上文中反复提到了类路径,这两个地方的类被加载时使用的类加载器明显不是同一个。也就是说,应用启动时,加载Drivder.class,使用的是PlatformClassLoader(以前叫ExtClassLoader ,JDK9之后改了),此时要加载类路径classpath中的诸如Mysql驱动,肯定是加载不到的。而加载类路径下的类,必须使用应用程序加载器,也就是AppClassLoader。

而通过“逆向”的双亲委派机制可以解决。

在这里插入图片描述

将应用程序类加载器设置进线程里面,即线程里面新定义一个类加载器的属性ContextClassLoader,然后将线程的ContextClassLoader这个属性设置为应用程序类加载器。然后启动类加载器去加载java.sql.Driverjava.sql.DriverManager等类时,同时也会从当前线程中取出这个ContextClassLoader即应用程序类加载器去classpath中加载外部厂商提供的JDBC驱动类。这个ContextClassLoader线程上下文类加载器

    @CallerSensitive
    public static <S> ServiceLoader<S> load(Class<S> service) {
        //从当前线程获取应用程序加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }
//以下代码摘自java.util.ServiceLoader.LazyClassPathLookupIterator#nextProviderClass
try {
    //此时loader=jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
    String fullName = PREFIX + service.getName();
    if (loader == null) {
        configs = ClassLoader.getSystemResources(fullName);
    } else if (loader == ClassLoaders.platformClassLoader()) {
       //略
    } else {
        //最终会走到这里,即,使用拼接之后的全路径来加载类路径中的配置信息
        //fullName=META-INF/services/java.sql.Driver
        configs = loader.getResources(fullName);
    }
} catch (IOException x) {
    fail(service, "Error locating configuration files", x);
}

五,JAVA的SPI与Dubbo的SPI区别

Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,二者的区别:

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值