初探java spi

什么是JDK SPI

SPI全称Service Provider Interface,是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。

JDK SPI 机制

当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。
所以spi需要几个关键点:

  • 有一个接口
  • 在 Classpath 下的 META-INF/services/ 创建一个以服务接口命名的文件
  • 在文件内写上对应实现的全类名

在这里插入图片描述

SPI 简单实现

通过一个简单的示例演示下 JDK SPI 的基本使用方式:

  • 有一个接口
    创建一个Log接口:
public interface Log {
    void log(String info);
}
  • 在 Classpath 下的 META-INF/services/ 创建一个以服务接口命名的文件
    创建对应的文件
    在这里插入图片描述

  • 在文件内写上对应实现的全类名
    写两个实现:
    log4j实现:

public class Log4j implements Log {

    @Override
    public void log(String info) {
        System.out.println("这是Log4j实现:" + info);
    }

}

logback实现:

public class Logback implements Log {

    @Override
    public void log(String info) {
        System.out.println("这是Logback实现:" + info);
    }

}

写个main方法执行对应实现:

public class SpiMain {

    public static void main(String[] args) {
        ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
        for (Log log : serviceLoader) {
            log.log("益达测试 SPI");
        }
    }

}

执行结果:
这是Log4j实现:益达测试 SPI
这是Logback实现:益达测试 SPI

源码分析

初始化

spi主要是通过java.util.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;

    public void reload() {
        // 清空
        providers.clear();
        // 实例化内部加载服务类
        lookupIterator = new LazyIterator(service, loader);
    }

private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // 要加载的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //访问控制器
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

load方法创建了一些属性(要加载的接口,类加载器,访问控制器),然后执行reload方法,reload主要实例化了内部类LazyIterator。load最后返回ServiceLoader的实例。

查找实现类

查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的iterator方法,iterator里面又是执行hasNext和next等方法,
hasNext主要执行hasNextService方法,
next方法主要执行nextService方法,

我们看看hasNextService和nextService方法:

private boolean hasNextService() {
            // 第二次调用已经解析完成直接返回true
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
                    //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        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();
            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
        }

到nextService的时候通过反射将对应的类进行返回.

应用场景

数据库驱动加载接口实现类的加载

JDBC加载不同类型数据库的驱动

数据库DriverManager类,它在静态代码块里面已经通过SPI机制, 把数据库驱动连接初始化了。它在里面查找的是Driver接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver

public class DriverManager {
 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;
        }
        // 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<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);
            }
        }
    }



在这里插入图片描述

而在com.mysql.jdbc.Driver类中,它做的主要就是在注册自身:

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

我们可以通过自定义Driver类来实现一些简单的扩展,在获取连接前打印连接信息:
新增自定义YidaDriver,我们需要继承原来的Driver,然后在resources下新建:
META-INF.services.java.sql.Driver文件文件内容写上我们自定义的com.study.springbootplus.config.YidaDriver全路径,
还需要将项目中配置的driver-class-name换成我们自定义的:


import com.mysql.jdbc.NonRegisteringDriver;

import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;

/**
 * @ClassName YidaDriver
 * @Author yida
 * @Date 2021/11/29 8:20 下午
 * @Description YidaDriver
 */
public class YidaDriver extends NonRegisteringDriver implements Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new YidaDriver());
        } 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 YidaDriver() throws SQLException {
        System.out.println("yida");

    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("准备创建数据库连接.url:"+url);
        System.out.println("JDBC配置信息:"+info);
        info.setProperty("user", "root");
        Connection connection =  super.connect(url, info);
        System.out.println("数据库连接创建完成!"+connection.toString());
        return connection;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }
}

在这里插入图片描述
最终启动打印日志出现:

yida
准备创建数据库连接.url:jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
JDBC配置信息:{user=root, password=root}
数据库连接创建完成!com.mysql.jdbc.JDBC4Connection@30ffb2a6

日志门面接口实现类加载

SLF4J加载不同提供应商的日志实现类

Spring

Servlet容器启动初始化org.springframework.web.SpringServletContainerInitializer

Spring Boot

自动装配过程中,加载META-INF/spring.factories文件,解析properties文件

Dubbo

Dubbo大量使用了SPI技术,里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来
例如Protocol 协议接口

dubbo 增强spi

dubbo spi源码分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值