SPI 使用手册

引入

我们知道每种数据库都是一个完整的独立的系统,假设我们现在的产品要兼容大多数数据库厂商,比如MySQL,Oracle,DB2等,那么怎么做呢?可以制作一些约定,大家都基于这个约定进行相关操作,比如调用者可以通过约定去连接数据库,而数据库厂商则根据这个约定实现连接数据库的具体操作。
在这里插入图片描述

这个看起来有点像设计模式中的门面模式,调用者只需要对这个约定进行相关操作,而具体的实现由相应的厂商实现,假设要从MySQL切换到DB2,只需要在底层悄悄的替换即可,那么具体是怎么替换的呢?

在maven项目中,假设我们目前使用的是MySQL,那么我们的maven依赖是不是有 mysql-connector-java ,换言之,我们可以把这个依赖替换成我们想要的数据库依赖

那么它到底是怎么实现替换依赖就可以切换数据库的呢
以MySQL为例,打开MySQL的jar,看到了META-INFcom 目录
在这里插入图片描述
进入META-INF/services/ 目录下,发现了 java.sql.Driver 文件
在这里插入图片描述
查看就发现了我们熟悉的驱动,而这个驱动就是MySQL厂商实现的
在这里插入图片描述
那他是怎么和我们的java应用程序相关联的呢,通过源码,我们发现是实现了java的Driver接口

package com.mysql.cj.jdbc; 

import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

这个接口正好是jdk的拓展包里的
在这里插入图片描述
现在可以知道上面描述的约定便是这个 java.sql.Driver 接口。大家都基于这个约定进行相应的操作
这个便是我们常说的SPI机制

SPI

什么是SPI

SPI的全名为Service Provider Interface,当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

JAVA中的实现

在Java中的实现就是ServiceLoader(源码部分省略)

public final class ServiceLoader<S>
    implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;
    
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
    .........
}

简单来说就是会在jar包下寻找 META-INF/services/ 目录下的文件,文件名就是接口全路径名,文件内容就是这个接口的实现类,可以有多个实现类。

解析文件的内容

private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));//必须是utf-8的格式
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }

在这里进行加载,并且放进缓存中

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        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
}

实战模拟

这是一个约定,在一个单独的模块中,打jar包

public interface IDataBase {
    String getDataBaseName();
}

这也是一个单独的模块,依赖上面制定的约定,实现DB2数据库,打jar包

public class DB2 implements IDataBase {
    public String getDataBaseName() {
        return "db2";
    }
}

这也是一个单独的模块,依赖上面制定的约定,实现MySQL数据库,打jar包

public class Mysql implements IDataBase{
    public String getDataBaseName() {
        return "mysql";
    }
}

在这里插入图片描述

测试

客户端依赖

<dependencies>
        <dependency>
            <groupId>com.luo</groupId>
            <artifactId>db-interface</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--使用DB2-->
        <dependency>
            <groupId>com.luo</groupId>
            <artifactId>db2</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
public class Test {
    public static void main(String[] args) {
        ServiceLoader<IDataBase> dataBases = ServiceLoader.load(IDataBase.class);//加载指定类的所有实现
        Iterator<IDataBase> iterator = dataBases.iterator();
        while (iterator.hasNext()){  //遍历每一个实现
            IDataBase next = iterator.next();
            System.out.println(next.getDataBaseName());
        }
    }
}

因为我只依赖了DB2,所以输出db2
在这里插入图片描述

优缺点

优点:解耦合,懒加载

缺点:会一次性加载配置文件中的所有实现类

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗罗的1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值