Java SPI机制

Java SPI机制

什么是SPI

image-20201013214652517

在编码过程中,我们经常会按照上图的方式进行编码,通常会定义一个接口,然后定义该接口的实现类。在使用时会“父类引用指向子类对象”的方式使用。这是在类的层面比较直观的一种展示。我们不妨在放开眼界,在模块,应用层面思考一下。

image-20201013220951922

我们对上面类关系进行放大,我们一个模块或应用依赖一组接口,比如JDBC、日志框架等

上图我们发现接口是介于调用方和具体实现的中间层,那什么时候接口更偏向与调用方,什么时候接口更偏向与实现方???

  1. 接口属于实现方的情况,也就是实现方提供了接口和实现,我们可以引用接口达到调用某实现类的功能,这就是我们常说的API,它具有以下特征:
    • 概念上更接近实现方
    • 组织上位于实现方所在的包中
    • 实现和接口在一个包中
  2. 接口属于调用方时,我们称其为SPI,全称service provider interface
    • 概念上依赖调用方
    • 组织上位于调用方所在的包中
    • 实现位于独立的包中

可能上面的解释还是比较难以理解,那我们就说点人话。我们假设接口就是一种服务,只不过它不提供真正服务,只是定义了功能。

  • 所谓SPI是站在 服务使用方 角度提出的“接口要求”,是对“服务提供方”提出的约定,简单说就是:“我需要这样的服务,现在你们来满足”。
  • 所谓API是站在 服务提供方 角度提供的无需了解底层细节的操作入口,即“我有这样的服务可以给你使用”。

为什么使用SPI

在第一节中,我们说过“父类引用指向子类对象”的方式,这种方式还是存在“硬编码”,而SPI提供了一种机制使得使用方与接口实现方完全解耦(后面介绍)。

image-20201013221201435

作为开发人员,我们知道需求一直在变,所以开发人员一直在追求当需求在发生变更时我如何很少改动代码,甚至不改动代码就能满足变化后的需求。或者我只进行扩展,而不是修改代码(开闭原则)。而SPI就是一种只需引入新的实现,不需更改一行代码,就能彻底替换掉底层的实现。

image-20201013221509253

通过上面的图示,我们可以看到应用调用方和接口始终是不变的,而变化的仅仅是接口的实现方,这样就保证了只要我们依赖的接口不发生改变,我们程序是不需要改动的,一般接口都是顶级抽象,都是比较稳定的,很少发生改变。

其实SPI的思想就是解耦,只不过它是完全解耦,提供了一种“可插拔”的方式。

SPI原理

在jdk6中引入了一种机制,提供了一个ServiceLoader类,通过官方API文档可以知道:它主要是用来装载一系列的service provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

模拟官方Demo

创建接口(服务)
public interface CodecSet {
    Encoder getEncoder(String encodingName);
    Decoder getDecoder(String encodingName);
}
//提供解码功能的接口
interface Decoder {}
//提供编码功能的接口
interface Encoder {}
创建接口的实现(服务的具体实现)
public class StandardCodecs implements CodecSet {
    @Override
    public Encoder getEncoder(String encodingName) {
        return new DefaultEncoder();
    }
    @Override
    public Decoder getDecoder(String encodingName) {
        return new DefaultDecoder();
    }
}
//编码的默认实现
class DefaultEncoder implements Encoder {}
//解码的默认实现
class DefaultDecoder implements Decoder {}
创建配置文件(发现服务的约定)
  1. 在classpath创建META-INF/services/目录
  2. 创建一个名字为com.keminapera.jdkapi.spi.CodecSet文件(该文件名就是需要加载服务接口的全限定名称)
  3. 文件的内容就是接口的具体实现类的全限定名称

image-20201013211600944

创建工厂(服务发现机制)
public class CodecsFactory {
    private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);

    public static Encoder getEncoder(String encodingName) {
        for (CodecSet cp : codecSetLoader) {
            Encoder enc = cp.getEncoder(encodingName);
            if (enc != null) {
                return enc;
            }
        }
        return null;
    }
}
测试类
public class SpiTest {
    @Test
    public void test() {
        Encoder encoder = CodecsFactory.getEncoder(StandardCharsets.UTF_8.name());
        System.out.println(encoder);
    }
}

通过模拟官方的demo,会发现,我们没有创建任何对象,仅仅通过CodecsFactory类我们就能获取到我们想要的实例对象,CodecsFactory类里面通过JDK提供的ServiceLoader类实现的,我们一起看看ServiceLoader类的具体实现。

ServiceLoader类

官方接口文档

image-20201013204442213

其实官方API文档已经写的很清楚了,总共对外提供了5个方法:

  • iterator():Iterator :以懒加载的方加载可用的providersServiceLoader
  • load(Class<> service):ServiceLoader:通过给定的类型创建了一个ServiceLoader对象,默认使用当前线程的上下文类加载器加载
  • load(Class<> service, ClassLoader loader):ServiceLoader:通过给定类型创建一个ServiceLoader对象,使用给定的类加载器加载
  • loadInstalled(Class<> service): ServiceLoader:通过给定类型创建一个ServiceLoader对象,使用扩展类加载器加载
  • reload():void:清除ServiceLoader缓存并重新加载

通过给定的API,我们可以发现其中有3个都是提供的静态工程方法用来创建ServiceLoader对象的,只有iterator和reload方式才是真正去加载provider,所以我们重点看看这两个方法的实现

核心代码

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //private static final String PREFIX = "META-INF/services/"; //定义的前缀   
            //Class<S> service; 就是传入需要加载的接口对应的Class
            String fullName = PREFIX + service.getName();
            //ClassLoader loader; loader指定类加载器
            //Enumeration<URL> configs = null; 需要加载的文件路径
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    //Iterator<String> pending = null; 
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    // String nextName = null;
    nextName = pending.next();
    return true;
}

JDBC驱动加载

添加MySql依赖

通过上面SPI原理的分析,我们看看JDBC中是如何使用SPI的,我们引入mysql依赖包,在依赖包中找到/META-INF/services/java.sql.Driver文件,该文件存放的就是需要加载的“服务提供者”。

image-20201015203937309

MySql中Driver实现类

该类在静态代码块中仅仅向DriverManager类里面注册了一个驱动

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

原始JDBC方式注册驱动

想想之前我们使用JDBC编程的是什么样子???是不是首先需要通过Class.forName()注册驱动

public class Main {
    //第二步:说明JDBC驱动的名称和数据库的地址
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost:3306/test";
 
    //第三步:说明数据库的认证账户及密码
    static final String USER = "root";
    static final String PASS = "123456";
 
    public static void main(String[] args) {
        //第四步:注册JDBC驱动
        try {
            Class.forName(JDBC_DRIVER);
        } catch (ClassNotFoundException e) {
            //这里会发生类没有找到的异常!
            e.printStackTrace();
        }
        //第五步:获得数据库连接
        try {
            Connection connection =  DriverManager.getConnection(DB_URL,USER,PASS);
            //第六步:执行查询语句
            Statement statement = connection.createStatement();
            String sql = "SELECT * FROM crawler_article";
            ResultSet rs =statement.executeQuery(sql);
            while (rs.next())
            {
                String title = rs.getString("title");
                String author = rs.getString("author");
                System.out.println(title+":"+author);
            }
            //第七步:关闭连接资源
            rs.close();
            statement.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
            //这里会发生SQL异常,因为我们提供的的账户和密码不一定能连接成功
        }
    }
}

原始JDBC注册驱动大致流程

image-20201015214653107

SPI方式注册驱动

public class DriverManager {
    //当该类被加载到JVM--在初始化阶段会执行静态代码块
	static {
        loadInitialDrivers();  
        println("JDBC DriverManager initialized");
    }
    
    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() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); //使用到了SPI机制
                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);
            }
        }
    }
}

通过对上面流程分析,我们可以得到下面的一个大致流程图

image-20201015214042013

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值