一篇文章搞明白Java中的SPI机制

这篇具有很好参考价值的文章主要介绍了一篇文章搞明白Java中的SPI机制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


一、什么是SPI机制?

SPI机制是Java的一种服务发现机制,为了方便应用扩展。那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制。这就是SPI机制( Service Provider Interface)

SPI机制在Java中有很广泛的运用,比如:eclipse和idea里的插件使用就是通过SPI机制实现的。开发工具提供一个扩展接口,具体的实现由插件开发者实现,开发工具提供一种服务发现机制来找到具体插件的实现,这就达到了插件的安装效果。从而可以使用插件服务。如果不需要某一插件,只需要删除某一插件的实现类,开发工具找不到具体的插件实现,这就达到了插件的卸载效果。不管是安装还是卸载都不会影响其他代码,其他服务。非常方便的实现了可插拔的效果。

JDBC中数据库连接驱动也使用了SPI机制,来达到适配不同DB数据库的效果。

SPI机制除了在jdk里有运用,在springboot中也用到了。springboot自动装配中"查找spring.factories 文件步骤"就是基于SPI的部分设计思想实现的。


二、JDK中SPI机制的实现原理

2.1 线程上下文类加载器

先来思考一个问题:上面说了SPI是一种服务发现机制,接口提供者需要提供一种能力来找到接口实现类。有这样一种场景:Java为我们定义了用于连接数据库的Driver接口。Mysql为我们提供了Driver接口的实现类mysqlDriver用于连接mysql数据库。

根据类加载的双亲委派原理得知,jvm在加载java.sql.Driver类时,会优先给Bootstarp类加载器去加载。但是Bootstarp类加载器只会加载jdk下的jar包和类(虚拟机按名称识别,不在虚拟机识别文件列表中的jar包不会加载)。而mysql提供的具体驱动程序实现类则是外部jar包。

上面这种情况下请问Java是怎么加载到mysql驱动从而连接mysql数据库的呢?

先说答案:通过线程上下文类加载器实现。

为了解决上面说的问题,Java设计团队引入了一个不怎么优雅的设计(破坏了虚拟机类加载时的双亲委派模型):为每个线程设置一个类加载器属性。该属性默认赋值Application(应用程序)类加载器。也可以通过下面这种方式设置自定义的类加载器

        //获取当前线程
        Thread thread = Thread.currentThread();
        //获取线程上下文类加载器
        ClassLoader classLoader = thread.getContextClassLoader();
        //设置线程上下文类加载器
        thread.setContextClassLoader(null);

因为默认的是Application类加载器,所以使得虚拟机在加载java.sql.Driver类时,可以通过当前线程,获取Application类加载器,然后找到第三方jar包。这样上面的问题解决了

上面这些,就是JDK中实现SPI机制的核心依赖点。

2.2 ServiceLoader

ServiceLoader是JDK提供的专门用于实现SPI机制的类。位于java.util.ServiceLoader

ServiceLoader类的构造函数被私有化了。所以构建ServiceLoader对象只能通过ServiceLoader.load()方法。该方法有两个重载

/**
service:需要加载的Class
loader:加载service用的类加载器
**/
 public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
     //可传入自定义类加载器
        return new ServiceLoader<>(service, loader);
    }

 /**
service:需要加载的Class
**/
 public static <S> ServiceLoader<S> load(Class<S> service) {
 //不传入类加载器,默认为当前上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

使用ServiceLoader时可选择是否用自定义类加载器来加载目标类。也可默认使用应用程序类加载器加载。


一篇文章搞明白Java中的SPI机制
jdk通过ServiceLoader类去ClassPath下的 “META-INF/services/”(此路径约定成俗) 路径里查找相应的接口实现类。ServiceLoader类核心功能就两个点,都在ServiceLoader的内部类LazyIterator中:

  • 查找相应接口对应实现类:hasNextService()
  • 加载相应接口实现类到虚拟机内:nextService()

查找核心逻辑如下:

private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                //前缀(META-INF/services/)+相应接口全限定名
                    String fullName = PREFIX + service.getName();
//该loader是构造ServiceLoader类时设置。可传入自定义类加载器,如未传入,则默认应用程序类加载器
                    if (loader == null)
                    //如果当前线程上下文类加载器为空,按照默认的双亲委派机制去寻找实现类资源配置。
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                    //在系统中查找资源,注意查找资源的加载器是从当前线程上下文中获取。也就是默认的应用程序类加载器。所以能加载到第三方jar包下的classpath路径。
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

加载

 private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            //hasNextService(查找)方法里获取到的第三方实现类全限定名。
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            //通过loader加载第三方实现类
                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
        }

2.3 小总结

JDK中的SPI实现,是由ServiceLoader类根据自定义传入类加载器或者应用程序类加载器在约定好的固定路径下(ClassPath:META-INF/services/)去查找和加载第三方接口实现类。

注意:要使用JDK中的SPI机制有几个前提条件

  • 服务提供方必须实现目标接口
  • 服务提供方必须在自身ClassPath:META-INF/services/路径下建立文件,文件名为目标接口全限定名。文件内容为实现目标接口的具体实现类全限定名

三、从JDBC的角度分析SPI机制

SPI机制在JDBC的使用主要是在获取数据库驱动的时候。依照SPI实现,我们来看一下JDBC是如何加载Mysql驱动程序的。

首先看下第一个条件:jdk定义Driver接口。mysq驱动程序提供接口实现类。
一篇文章搞明白Java中的SPI机制
mysql中的Driver类,确实是实现了java.sql.Driver接口

第二个条件:接口实现类配置文件,必须放在ClassPath:META-INF/services/路径下
一篇文章搞明白Java中的SPI机制

条件都满足ok,下面来具体看一下真正的加载实现逻辑。

3.1 获取驱动程序实现类列表

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

逻辑体现在上面代码中的DriverManager驱动管理器里:java.sql.DriverManager
一篇文章搞明白Java中的SPI机制

DriverManager驱动管理器核心功能点是static代码块下的loadInitialDrivers()方法调用。它会去注册通过jdbc.properties指定的数据库驱动程序通过ServiceLoader去加载可能存在的第三方数据库驱动程序。

 private static void loadInitialDrivers() {
        String drivers;
        try {
        //获取系统属性中设置的数据库驱动程序(类全限定名)
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
//通过ServiceLoader.load()方法去加载第三方jar包下的数据库驱动程序实现类。使用的是默认的应用程序类加载器
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                //由于ServiceLoader类下的内部类LazyIterator实现了Iterator迭代器接口。所以可以遍历处理获取到的一个或多个第三方驱动程序实现类
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                //hasNext会调用hasNextService方法,用于查找第三方数据库驱动实现类
                    while(driversIterator.hasNext()) {
                    //next会调用nextService方法,用于加载第三方数据库驱动实现类
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                //将系统设置jdbc.properties获取到的数据库驱动程序加载进虚拟机中
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

3.2 如何正确选择Mysql驱动程序

由于一个应用要可以连接mysql,也同时可以连接oracle。所以DriverManager获取到的驱动程序可能有多个。那使用的时候怎么才能选择期望的数据库驱动程序呢?

先说答案:遍历所有驱动程序,根据数据库url一个一个尝试获取数据库连接,获取成功说明驱动程序是期望的。

验证答案:得回到这段代码上来:DriverManager.getConnection()

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

DriverManager获取数据库连接方法核心调用:

 private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
           
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

       
        SQLException reason = null;
//开始遍历所有注册的数据库驱动程序
        for(DriverInfo aDriver : registeredDrivers) {
           
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success! 获取成功,返回连接
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }
//如果获取不到任何数据库连接,抛出sql异常
        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

以上就是JDK中SPI在JDBC的运用分析。文章来源地址https://www.toymoban.com/news/detail-480711.html

到了这里,关于一篇文章搞明白Java中的SPI机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【操作系统】一篇文章带你理清Linux中的权限!

    🎬 乀艨ic: 个人主页 ⛺️说是高产但是还是过了快半个月才更新() ⭐️来看看这次的博客吧~ 上次注意到发Linux相关的点击量比其他的多很多,那就最近多更几篇Linux相关的吧() 注:Linux的不同发行版本的指令可能有所不同,本次是按照CentOS7的标准来进行追述的。 在谈

    2024年04月11日
    浏览(48)
  • java注解,一篇文章就够了

    Annotation(注解)是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能。 Annotation(注解)也被称为元数据(Metadata)是JDK1.5及以后版本引入的,用于修饰 包、类、接口、字段、方法参数、局部变量 等。 常见的注解如:@Override、@Deprecated和@SuppressWarnings 2.1 使用步

    2024年02月03日
    浏览(48)
  • 一篇文章让你搞懂TypeScript中的typeof()、keyof()是什么意思

    知识专栏 专栏链接 TypeScript知识专栏 https://blog.csdn.net/xsl_hr/category_12030346.html?spm=1001.2014.3001.5482 有关TypeScript的相关知识可以前往TypeScript知识专栏查看复习!! 最近在 前端的深入学习过程 中,接触了与 网络请求 相关的内容,于是计划用三个专栏( HTTP 、 Axios 、 Ajax )和零碎

    2023年04月21日
    浏览(58)
  • Vue中的Pinia状态管理工具 | 一篇文章教会你全部使用细节

    Pinia(发音为/piːnjʌ/,如英语中的“peenya”)是最接近piña(西班牙语中的菠萝)的词 ; Pinia开始于大概2019年,最初是 作为一个实验为Vue重新设计状态管理 ,让它用起来适合组合式API(Composition API)。 从那时到现在,最初的设计原则依然是相同的,并且目前同时兼容Vue2、

    2024年02月11日
    浏览(39)
  • 一篇文章带你走进Java(保姆级)

    手打不易,希望对各位还在徘徊学什么语言的有帮助!!java不会让你失望!! Java是一种优秀的程序设计语言,它具有令人赏心悦目的语法和易于理解的语义。 Java还是有一系列计算机软件和规范形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持

    2024年02月15日
    浏览(53)
  • 【JavaSE】一篇文章领悟Java运算符

    前言: 作者简介: 爱吃大白菜1132 人生格言:纸上得来终觉浅,绝知此事要躬行   如果文章知识点有错误的地方不吝赐教,和大家一起学习,一起进步!   如果觉得博主文章还不错的话,希望三连支持! 目录 什么是运算符  1.算数运算符  基本四则运算符:加减乘除模(+ -

    2024年02月08日
    浏览(45)
  • 一篇文章告诉你什么是Java内存模型

    在上篇 并发编程Bug起源:可见性、有序性和原子性问题,介绍了操作系统为了提示运行速度,做了各种优化,同时也带来数据的并发问题, 在单线程系统中,代码按照顺序 从上往下 顺序执行,执行不会出现问题。比如一下代码: 程序从上往下执行,最终 c 的结果一定会是

    2024年02月06日
    浏览(42)
  • 高级编程JavaScript中的Map键值对你知道吗?一篇文章看懂

    Map 保存键值对,其中键可以是任何数据类型。 Map 会记住键的原始插入顺序。 Map 提供表示映射大小的属性。 方法 描述 new Map() 创建新的 Map 对象。 set() 为 Map 中的键设置值。 get() 获取 Map 对象中键的值。 clear() 从 Map 中移除所有元素。 delete() 删除由某个键指定的 Map 元素。

    2024年01月25日
    浏览(46)
  • 一篇文章搞定Java中常用集合的排序方法

    目录 Array · 数组 List · 列表 Collections.sort() 简单类型 复杂对象 类 使用Lambda表达式 Stream API Map · 键值对 对 Map 的 Key 进行排序 对 Map 的 Value 进行排序 最近在做算法题的时候,发现排序在大部分题中都不可或缺,今天心血来潮,总结下Java中集合排序常用的方法,基本覆盖了大

    2024年02月09日
    浏览(51)
  • 一篇文章带你彻底弄懂Java的==符号

    本篇文章6735字,大概阅读时间20分钟。本文中使用到的JDK版本为1.8.0_301 目录 ==符号的定义 基本类型中==符号的判断 String类型中==符号的判断         在Java中==符号的作用分为两类:         1:==符号在八种基本类型的作用是比较对应基本类型的 数值是否相等         2:

    2024年02月08日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包