面试官提问:你了解SPI机制吗?你知道它在那些场合被用到吗?

SPI机制

什么是SPI?

SPI,全称Service Provider interface,即服务提供发现接口。(本质上就是面向接口编程)

它能通过在classpath/META-INF/services文件夹中查找文件,自动加载文件中定义的类。

Tips1: 虽然平时并不会直接使用到 SPI 来实现业务,但其实我们使用过的绝大多数框架都会提供 SPI 接口方便使用者扩展自己的功能。

常见的如Dubbo,它提供了一系列的拓展

在这里插入图片描述

此外在JDBC中也使用了SPI机制

在这里插入图片描述

例子

我们先定义一个接口(我们通过该接口发现路径下的相关服务)

public interface SPIService{
    void excute();
}

定义两个服务A,B,实现SPIService接口

public class A implements SPIService{
    void excute(){
        System.out.println("A执行");
    }
}
public class B implements SPIService{
    void excute(){
        System.out.println("B执行");
    }
}

最后我们在classpath/META-INF/services文件夹中

文件名为接口的全限定名: com.test.spi.service.SPIService

文件内容为接口实现类的全限定类名,多个用换行符隔开:

com.test.spi.service.Impl.A
com.test.spi.service.Impl.B

在这里插入图片描述

这样子我们可以根据SPI机制,**通过读取相关路径信息,启动所需要的服务。**写个测试类

我们有两种方式可以实现,一种是通过ServiceLoad.load()方法,由java.util包提供

另一种是通过Service.providers方法拿到实现类的实例,由sum.misc.Service提供

public class TestSPI {
    public static void main(String[] args) {
        FirstCallOfSPI();
        System.out.println("---------------");
        SecondCallOfSPI();
    }

    //Service.providers()
    private static void FirstCallOfSPI() {
        Iterator<SPIService> providers =
                Service.providers(SPIService.class);
        while (providers.hasNext()) {
            SPIService next = providers.next();
            next.excute();
        }
    }

    //ServiceLoader.load()
    private static void SecondCallOfSPI() {
        ServiceLoader<SPIService> load =
                ServiceLoader.load(SPIService.class);
        Iterator<SPIService> iterator = load.iterator();
        while (iterator.hasNext()) {
            SPIService next = iterator.next();
            next.excute();
        }
    }
}

执行结果

在这里插入图片描述

源码解析

这里主要说一下ServiceLoad,由java.util提供(sum.misc.Service看不了源码)

ServiceLoad类

在这里插入图片描述

load()

这步主要是实例化了内部类:LazyIterator,并返回ServiceLoader的实例

在这里插入图片描述

Tips2: AccessController的作用(看源码最害怕多几个不认识的名词了。。)

简单介绍下 // todo 有机会深入了解Java安全策略的话就会更懂了

AccessController 类用于与访问控制相关的操作和决定。

更准确的说:AccessController类用于以下三个目的

  • 基于当前的Java安全策略决定是允许还是拒绝对关键系统资源的访问
  • 将代码标记为享有“特权”,从而影响该代码后续的资源访问决定
  • 获取当前调用上下文的”快照",这样就可以相对于以保存的上下文做出其他上下文的访问控制决定。

一般通过调用checkPermission()根据特定算法进行权限判定

我们可以通过doPrivileged将调用方标记为特权只要调用方访问拥有权限的域,就不需要checkPermission权限判定了;但是如果过域了,通常直接抛出异常。

LazyIterator的内部细节

LazyIterator内部主要是查找实现类和创建实现类的过程。

其实当我们调用Iterator.hasNext()Iterator.next()的时候,都是在调用LazyIterator的相应方法

其中Iterator.hasNext()主要用于查找实现类

Iterator.next()主要用于创建实例

在这里插入图片描述

所以现在我们更加关注的是,LazyIterator是怎么重写这两个方法来进行实现类的查找和创建呢?
在这里插入图片描述

在这里插入图片描述

Tips3: PrivilegedAction的作用?

PrivilegedAction 是一个具有单个方法的接口

 public interface PrivilegedAction<T> {
 	T run();
 }

方法名为run,并返回一个Object。

一般该接口要配合AccessController进行权限控制代码进行使用,大概使用场景:

aboutingmethod(){
    // 普通代码写在这。。。
    AccessController.doPrivileged(new PrivilegedAction(){
        public Object run(){}
        //特权相关代码在这里编写,比如:
        System.out.println("awt");
        return null;
    	}
    });
    //普通代码...
}

上述事例显示了该接口的实现的创建,提供了run方法的具体实现。

调用doPrivileged时,将PrivilegedAction实现的实例传递给它。

doPrivileged 方法在启用特权后从 PrivilegedAction 实现调用 run 方法,并返回 run 方法的返回值作为 doPrivileged 的返回值、

在使用“特权”构造时务必 * 特别 * 小心,始终让享有特权的代码段尽可能的小。

查找实现类

调用Iterator.hasNext()最重要就是调用hasNextService()方法

在这里插入图片描述

创建实例

调用next方法的时候,实际调用的是lookupIterator.nextService()

它通过反射的方式,创建实现类的实例并返回。

在这里插入图片描述

到此,SPI机制如何读取/META-INF/services/目录下的文件

并将文件中的全限定类名进行初始化,获取到类的实例的一系列过程,我们都进行了简单的了解。

JDBC中的应用

SPI机制为很多框架的拓展提供了帮助和可能,在JDBC中就应用到了该机制。

在最初的JDBC,获取数据库连接的过程,需要先设置数据库驱动的连接,在通过DriverManager.getConnection获取一个Connnection

String url = "jdbc:mysql:///xxx"
String user = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, password);

在新版本中,设置数据库驱动连接,这一步骤就不需要了,而是通过SPI机制进行配置

加载

我们首先看看DriverManager类,它在静态代码块做了一件重要的事

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqMsUNZ2-1630160091127)(SPI机制/image-20210705192025700.png)]

显然,它已经通过SPI机制,将数据库驱动连接初始化了。

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;
    }
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//眼熟吧,SPI机制加载Driver接口的服务类,Driver接口的包为:java.sql.Driver
                //所以它要找的就是META-INF/services/java.sql.Driver文件
                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;
            }
        });
        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);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
获取Connection
private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
     * When callerCl is null, we should check the application's
     * (which is invoking this class indirectly)
     * classloader, so that the JDBC driver class outside rt.jar
     * can be loaded from here.
     (当callerCl是空,我们应该检查间接调用类的应用程序加载器,以便JDBC驱动器能够加载rt.jar外的类)
     */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        // synchronize loading of the correct classloader.
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

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

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

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;

    //这一段就是加载的核心
    registeredDrivers中就包含com.mysql.cj.jdbc.Driver实例
    for(DriverInfo aDriver : registeredDrivers) {
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                println("    trying " + aDriver.driver.getClass().getName());
                //调用connect方法创建连接
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    println("getConnection returning " + aDriver.driver.getClass().getName());
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }

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

    }

    // 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");
}

总结

Java 自身的 SPI 其实也有点小毛病,比如:

  • 遍历加载所有实现类效率较低。
  • 当多个 ServiceLoader 同时 load 时会有并发问题(虽然没人这么干),所以在JDBC上有一串对类加载器的处理,不知道是不是为了解决这个问题

最后总结一下,SPI 并不是某项高深的技术,本质就是面向接口编程,而面向接口本身在我们日常开发中也是必备技能,所以了解使用 SPI 也是很用处的。


参考文章:

深入理解SPI机制 - 简书 (jianshu.com)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值