SPI机制

JAVA拾遗--关于SPI机制

JDK提供的SPI(Service Provider Interface)机制,可能很多人不太熟悉,因为这个机制是针对厂商或者插件的,也可以在一些框架的扩展中看到。其核心类java.util.ServiceLoader可以在jdk1.8的文档中看到详细的介绍。虽然不太常见,但并不代表它不常用,恰恰相反,你无时无刻不在用它。玄乎了,莫急,思考一下你的项目中是否有用到第三方日志包,是否有用到数据库驱动?其实这些都和SPI有关。再来思考一下,现代的框架是如何加载日志依赖,加载数据库驱动的,你可能会对class.forName(“com.mysql.jdbc.Driver”)这段代码不陌生,这是每个java初学者必定遇到过的,但如今的数据库驱动仍然是这样加载的吗?你还能找到这段代码吗?这一切的疑问,将在本篇文章结束后得到解答。

首先介绍SPI机制是个什么东西

实现一个自定义的SPI

1 、项目结构

  1. jdbc是针对厂商和插件商定义的接口项目,只提供接口,不提供实现。
  2. jdbc-mysql,jdbc-oracle分别是两个厂商对jdbc的不同实现,所以他们会依赖于interface项目。
  3. test是我们的用来测试的主项目。

这个简单的demo就是让大家体验,在不改变test代码,只更改依赖的前提下,切换interface的实现厂商。

2 、jdbc模块

package www;
public interface DriverManager
{
     String dbType();
}

jdbc只定义一个接口,不提供实现。规范的制定方一般都是比较牛叉的存在,这些接口通常位于java,javax前缀的包中。这里的Printer就是模拟一个规范接口。

3 、jdbc-mysql模块

这个模块涉及了三个文件:

  1. DriverManagerMysql .java文件
    package www;
    public class DriverManagerMysql implements  DriverManager
    {
        @Override
        public String dbType() {
            return "mysql";
        }
    }
    

     

  2. www.DriverManager文件
    www.DriverManagerMysql

     

  3. pom.xml文件
     <dependencies>
            <dependency>
                <groupId>www</groupId>
                <artifactId>jdbc</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>

     

这里需要重点说明,每一个SPI接口都需要在自己项目的静态资源目录中声明一个services文件,文件名为实现规范接口的类名全路径,在此例中便是www.DriverManager,在文件中,则写上一行具体实现类的全路径,在此例中便是www.DriverManagerMysql

 

这样一个厂商的实现便完成了。

4 、jdbc-oracle模块

  1. DriverManagerOracle.java文件
    package www;
    public class DriverManagerOracle implements  DriverManager
    {
        @Override
        public String dbType() {
            return "oracle";
        }
    }
    

     

  2. www.DriverManager文件
    www.DriverManagerOracle

     

  3. pom.xml文件
     <dependencies>
            <dependency>
                <groupId>www</groupId>
                <artifactId>jdbc</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>

 

这样,另一个厂商的实现便完成了。

5 test模块

5.1 编写调用主类

 public static void main(String[] args) {

        ServiceLoader<DriverManager> serviceLoader = ServiceLoader.load(DriverManager.class);
        Iterator<DriverManager> iterator = serviceLoader.iterator();

        while (iterator.hasNext()) {
            DriverManager driverManager = iterator.next();
            System.out.println(driverManager.dbType());
        }


    }

 ServiceLoader是java.util提供的用于加载固定类路径下文件的一个加载器,正是它加载了对应接口声明的实现类。

5.2 测试1

这里的test便是我们自己的项目了。如果一开始我们想使用厂商jdbc-msyql实现,是需要将其的依赖引入。

<dependencies>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc-mysql</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
</dependencies>

 5.3 测试2

如果在后续的方案中,想替换厂商,只需要将依赖更换jdbc-oracle

<dependencies>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc-oracle</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
</dependencies>

调用主类无需变更代码,这符合开闭原则

 是不是很神奇呢?这一切对于调用者来说都是透明的,只需要切换依赖即可!

 5.4 测试3

serviceLoader对Iterable接口进行了实现,在main函数中 的确对serviceLoader进行了遍历,可遍历说明是可以有多个的。Test模块是不是可以同时引入jdbc-mysql和jdbc-oracle呢?

    <dependencies>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc-mysql</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>www</groupId>
            <artifactId>jdbc-oracle</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

 

 

SPI机制的约定:

  1.  在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为Api具体实现类的全限定名
  2. 使用ServiceLoader类动态加载META-INF中的实现类
  3. 如SPI的实现类为Jar则需要放在主程序classPath中
  4. Api具体实现类必须有一个不带参数的构造方法

                               

优点
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

缺点

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用ServiceLoader类的实例是不安全的。

 

 

SPI在实际项目中的应用

先总结下有什么新知识,resources/META-INF/services下的文件似乎我们之前没怎么接触过,ServiceLoader也没怎么接触过。那么现在我们打开自己项目的依赖,看看有什么发现。

在mysql-connector-java-5.1.6.jar中发现了META-INF\services\java.sql.Driver文件。注意mysql-connector-java的版本,不同的版本还是有区别的。

我们可以分析出,java.sql.Driver是一个接口,com.mysql.jdbc.Driver是实现类。

mysql-connector-java-6.0.6.jar:

 

既然说到了数据库驱动,索性再多说一点,还记得一道经典的面试题:class.forName(“com.mysql.jdbc.Driver”)到底做了什么事?

class.forName用于加载class字节码,在加载com.mysql.jdbc.Driver类的字节码时会触发其静态代码块的执行,从而使主类加载数据库驱动。JDBC规范中明确要求Driver类必须向DriverManager注册自己。

mysql-connector-java-5.1.6.jar

package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

 

class.forName(“com.mysql.jdbc.Driver”)时代的终结:

来到最新的DriverManager源码中,可以看到这样的注释,翻译如下:

JDBC 4.0 Drivers 必须包括 META-INF/services/java.sql.Driver 文件。此文件包含 java.sql.Driver 的 JDBC 驱动程序实现的名称。

例如:要加载java.sql.Driver的实现类 my.sql.Driver ,META-INF/services/java.sql.Driver 文件需要包含下面的条目:my.sql.Driver

应用程序不再需要使用 Class.forName() 显式地加载 JDBC 驱动程序。当前使用 Class.forName() 加载 JDBC 驱动程序的现有程序将在不作修改的情况下继续工作。

可以发现,Class.forName已经被弃用了。DriverManager 通过spi完成了Class.forName("com.mysql.jdbc.Driver")的任务:


public class DriverManager {
    static {
        loadInitialDrivers();
    }
	private static void loadInitialDrivers() {
        ……
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });
       ……
    }
}

当然那,本节的内容还是主要介绍SPI,驱动这一块这是引申而出,如果不太理解,可以多去翻一翻jdk1.8中Driver和DriverManager的源码,相信会有不小的收获。

参考复制:https://www.cnkirito.moe/spi/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值