java开发ServiceLoader实现机制及SPI应用

 更新时间:2022年10月28日 14:31:52   作者:梦想实现家_Z  
这篇文章主要为大家介绍了java开发ServiceLoader实现机制及SPI应用,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

前言

做过java web开发的小伙伴大多数时候都需要链接数据库,这个时候就需要配置数据库引擎DriverClassName参数,这样我们的java应用才能通过数据库厂商给的Driver与指定的数据库建立通信。但是这里就有一个疑问:

java.sql.Driver是jdk自带的接口,它是由BoostrapClassLoader加载的,DriverClassName是外部厂商提供的具体实现,是由AppClassLoader加载的,要建立与数据库的通信,必然是通过java.sql.Driver接口方法发起的,那么在java.sql.Driver是如何拿到具体实现的呢?它是不是违背了ClassLoader的双亲委派模式呢?

如何绕过双亲委派模式

为了拿到AppClassLoader中加载的java.sql.Driver实现类,我们可以查看一下DriverManager是怎么处理的:

public static Driver getDriver(String url)
        throws SQLException {
    println("DriverManager.getDriver("" + url + "")");
    ensureDriversInitialized();
    ......
}
private static void ensureDriversInitialized() {
    ......
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    // 核心代码ServiceLoader
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    try {
                        while (driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch (Throwable t) {
                        // Do nothing
                    }
                    return null;
                }
            });
    ......
}

我们最终可以发现,DriverManager通过ServiceLoader.load(Driver.class)就拿到了我们配置的DriverClassName实现类。这就实现在DriverManager中拿到了外部提供的Driver实现,绕过来双亲委派模式。

ServiceLoader实现机制

我们来看一下ServiceLoader是如何实现SPI机制的,先从ServiceLoader.load()方法入手:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 从当前线程中获取ClassLoader
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 创建一个ServiceLoader对象
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }

1.从当前线程中获取ClassLoader;因为在创建AppClassLoader后,将AppClassLoader设置进当前线程的上下文中;

2.根据ClassLoader以及目标接口类创建一个ServiceLoader对象;

其实ServiceLoader核心代码在hasNext()方法中:

       @Override
        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

最终都会调用到hasNextService()方法中:

private boolean hasNextService() {
    // nextProvider默认为null,如果通过next()取出来了,nextProvider就会变成null
    while (nextProvider == null && nextError == null) {
        try {
            // 找到目标实现类
            Class<?> clazz = nextProviderClass();
            if (clazz == null)
                return false;
            if (clazz.getModule().isNamed()) {
                // ignore class if in named module
                continue;
            }
            // 判断service接口是否和clazz有父子关系
            if (service.isAssignableFrom(clazz)) {
                Class<? extends S> type = (Class<? extends S>) clazz;
                // 获取无参构造函数
                Constructor<? extends S> ctor
                            = (Constructor<? extends S>)getConstructor(clazz);
                // 包装成一个ProviderImpl对象
                ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
                // 并赋值给nextProvider
                nextProvider = (ProviderImpl<T>) p;
            } else {
                fail(service, clazz.getName() + " not a subtype");
            }
        } catch (ServiceConfigurationError e) {
            nextError = e;
        }
    }
    return true;
}

外部提供的实现类一定要有一个无参构造函数,否则会导致ServiceLoader加载失败;

我们下面再继续深入看看ServiceLoader是怎么找到实现类的:

private Class<?> nextProviderClass() {
    if (configs == null) {
        try {
            // 拼接文件名:"META-INF/services/接口名称"
            // 比如接口名为:java.sql.Driver,
            // 那么文件路径就是:"META-INF/services/java.sql.Driver"
            String fullName = PREFIX + service.getName();
            // 没有指定ClassLoader,就通过getSystemClassLoader()加载目标文件
            if (loader == null) {
                configs = ClassLoader.getSystemResources(fullName);
            } else if (loader == ClassLoaders.platformClassLoader()) {
                // 如果是platformClassLoader,它没有class path,那么看看BootLoader有没有class path
                if (BootLoader.hasClassPath()) {
                    configs = BootLoader.findResources(fullName);
                } else {
                    configs = Collections.emptyEnumeration();
                }
            } else {
                // 通过指定classLoader加载目标文件
                configs = loader.getResources(fullName);
            }
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    // 上面代码只会执行一次,这样configs就不会为null,下次进来直接取下一个实现类
    // 把configs内容解析成一个迭代器
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return null;
        }
        pending = parse(configs.nextElement());
    }
    // 通过迭代器获取下一个实现类名称
    String cn = pending.next();
    try {
        // 通过类名反射成Class对象
        return Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service, "Provider " + cn + " not found");
        return null;
    }
}

1.实现类的载入是因为在META-INF/services/文件夹中创建了以目标接口名称命名的文件,并在里面写上了实现类的全路径类名。

2.ServiceLoader通过ClassLoader从class path中载入目标文件里面的内容,并解析出实现类的全路径类名;

3.最终通过反射的方式创建出实现类的Class对象,这样就完成了SPI的实现;

SPI在各个框架上的应用

除了在数据库Driver上使用了SPI,我们还可以发现SPI在各个框架上都有大量的应用。比如我最近在看的Seata分布式事务框架,里面就有用到SPIio.seata.common.loader.EnhancedServiceLoader

另一个就是我们经常使用的mysql-connector-java以及阿里的Druid:

小结

通过以上源码分析以及示例演示,我们简单做一个小结:

1.ServiceLoader打破双亲委派模式的方式通过获取当前线程上下文中的ClassLoader完成的;

2.SPI的实现类名称必须放在META-INF/services/文件夹下面,以目标接口名称作为文件名称,文件内容为目标实现类全路径类名;

3.目标实现类必须要有一个无参构造函数,否则SPI会失败;

以上就是java开发ServiceLoader实现机制及SPI应用的详细内容,更多关于java开发ServiceLoader SPI的资料请关注脚本之家其它相关文章!

相关文章

  • java虚拟机

    java虚拟机

    2008-01-01
  • Java之int数组声明与初始化方式

    Java之int数组声明与初始化方式

    这篇文章主要介绍了Java之int数组声明与初始化方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • SpringBoot整合Swagger2实例方法

    SpringBoot整合Swagger2实例方法

    在本篇文章里小编给大家整合了关于SpringBoot整合Swagger2的相关知识点内容,有兴趣的朋友们学习下。
    2019-06-06
  • 详解Spring工厂特性

    详解Spring工厂特性

    今天带大家学习Spring的特性-工厂特性,文中有非常详细的介绍及代码示例,对正在学习java的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-05-05
  • LambdaQueryWrapper与QueryWrapper的使用方式

    LambdaQueryWrapper与QueryWrapper的使用方式

    这篇文章主要介绍了LambdaQueryWrapper与QueryWrapper的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Java 线程对比(Thread,Runnable,Callable)实例详解

    Java 线程对比(Thread,Runnable,Callable)实例详解

    这篇文章主要介绍了Java 线程(Thread,Runnable,Callable)实例详解的相关资料,这里对java 线程的三种方法进行了对比,需要的朋友可以参考下
    2016-12-12
  • java设计模式学习之装饰模式

    java设计模式学习之装饰模式

    这篇文章主要为大家详细介绍了java设计模式学习之装饰模式的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-10-10
  • @DS注解的使用,动态数据源,事务详解

    @DS注解的使用,动态数据源,事务详解

    在项目中使用多数据源时,可以借助苞米豆的dynamic-datasource-spring-boot-starter进行配置,首先需引入相应的jar包,并在application.yml中设置主从数据源,其中一般选择master作为默认数据源,在实现类中通过@DS注解指定数据源
    2024-09-09
  • SpringBoot中支持Https协议的实现

    SpringBoot中支持Https协议的实现

    本文主要介绍了SpringBoot中支持Https协议的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • Java基础学习之标签

    Java基础学习之标签

    在Java中,标签必须在循环之前使用, 一个循环之中嵌套另一个循环的开关,从多重嵌套中continue或break,该文详细介绍了标签的相关知识,对正在学习java基础的小伙伴们还很有帮助,需要的朋友可以参考下
    2021-05-05

最新评论