• Java -- JDK中SPI机制


    SPIService Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件

    Java SPI是一种以接口为基础,使用配置文件来加载(或称之为服务发现)的动态加载机制,主要使用JDK中 java.util.ServiceLoader 来实现

    SPI是一种动态替换发现的机制,比如有个接口,想运行时动态的给它添加实现,只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和SQLServer,Oracle,Postgresql 等都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实

    一、SPI使用

    使用SPI比较简单,只需要按照以下几个步骤

    1、创建一个接口

    2、在jar包的 META-INF/services 目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名(SPI的实现类必须带一个无参构造方法

    3、以接口的全路径做为名称,创建文件(注意不要带文件格式后缀)

    4、在文件中写入实现类的全路径

    5、在代码中调用ServiceLoader.load(接口.class)获取迭代器

    6、遍历迭代器即可获取对应实现类的实例,通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM

    如 MySQL驱动类

    java.sql.Driver文件全部内容

    1. public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    2. public Driver() throws SQLException {
    3. }
    4. static {
    5. try {
    6. DriverManager.registerDriver(new Driver());
    7. } catch (SQLException var1) {
    8. throw new RuntimeException("Can't register driver!");
    9. }
    10. }
    11. }

    触发顺序为:

    1、DriverManager在静态块中执行方法loadInitialDrivers()

    2、loadInitialDrivers()中会有地方调用ServiceLoader.load(Driver.class)

    3、触发hasNext()时,由于com.mysql.jdbc.Driver被ClassLoder扫到于是执行静态块,实例化一个自己并注册到DriverManager

    二、SPI加载过程

    ServiceLoader位于java.util.ServiceLoader,所以这个类是会被启动类加载器所加载

    1、首先找到ServiceLoader中的静态方法:load()

    根据类加载的原理:如果一个类由类加载器A加载,那么这个类的依赖类也会被类加载器A加载(前提是这个依赖类尚未被加载过) 

    执行ServiceLoader.load(Driver.class),如果不使用线程上下文类加载器来打破双亲委托模型,那么该方法的关联类也会被启动类加载器加载

    1. public static ServiceLoader load(Class service) {
    2. ClassLoader cl = Thread.currentThread().getContextClassLoader();
    3. return ServiceLoader.load(service, cl);
    4. }

    (1)初始化了一个:ServiceLoader对象,new ServiceLoader<>(service, loader)

    (2)执行reload()

    (3)new LazyIterator(service, loader) 

    2、new了一个ServiceLoader的实例,并执行reload();调用另一个重载的load方法去加载Driver(即所谓的Service)

    1. private ServiceLoader(Class svc, ClassLoader cl) {
    2. service = Objects.requireNonNull(svc, "Service interface cannot be null");
    3. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    4. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    5. // 实例化中reload
    6. reload();
    7. }

    3、在reload中创建了一个懒加载迭代器

    1. public void reload() {
    2. providers.clear();
    3. // 懒加载迭代器
    4. lookupIterator = new LazyIterator(service, loader);
    5. }

    4、当这个迭代器被调用hasNext时,跟着代码走下去,会发现有个PREFIX

    1. // 迭代方法
    2. public boolean hasNext() {
    3. if (acc == null) {
    4. return hasNextService();
    5. } else {
    6. PrivilegedAction action = new PrivilegedAction() {
    7. public Boolean run() { return hasNextService(); }
    8. };
    9. return AccessController.doPrivileged(action, acc);
    10. }
    11. }
    12. private boolean hasNextService() {
    13. if (nextName != null) {
    14. return true;
    15. }
    16. if (configs == null) {
    17. try {
    18. // 由PREFIX + name获取了文件全路径
    19. String fullName = PREFIX + service.getName();
    20. if (loader == null)
    21. configs = ClassLoader.getSystemResources(fullName);
    22. else
    23. configs = loader.getResources(fullName);
    24. } catch (IOException x) {
    25. fail(service, "Error locating configuration files", x);
    26. }
    27. }
    28. while ((pending == null) || !pending.hasNext()) {
    29. if (!configs.hasMoreElements()) {
    30. return false;
    31. }
    32. pending = parse(service, configs.nextElement());
    33. }
    34. nextName = pending.next();
    35. return true;
    36. }

    5、指向了相对路径下的META-INF/services/

    private static final String PREFIX = "META-INF/services/";
    

    6、在Iterator遍历时,通过PREFIX + service.getName(),使用ClassLoader.getSystemResources(fullName)获取了配置里的内容,然后通过读文件的方式加载到自己的缓存,然后再调用迭代器的next()时,就会将其实例化

    1. public S next() {
    2. if (acc == null) {
    3. return nextService();
    4. } else {
    5. PrivilegedAction action = new PrivilegedAction() {
    6. public S run() { return nextService(); }
    7. };
    8. return AccessController.doPrivileged(action, acc);
    9. }
    10. }
    11. private S nextService() {
    12. if (!hasNextService())
    13. throw new NoSuchElementException();
    14. String cn = nextName;
    15. nextName = null;
    16. Class c = null;
    17. try {
    18. c = Class.forName(cn, false, loader);
    19. } catch (ClassNotFoundException x) {
    20. fail(service,
    21. "Provider " + cn + " not found");
    22. }
    23. if (!service.isAssignableFrom(c)) {
    24. fail(service,
    25. "Provider " + cn + " not a subtype");
    26. }
    27. try {
    28. // 实例化
    29. S p = service.cast(c.newInstance());
    30. providers.put(cn, p);
    31. return p;
    32. } catch (Throwable x) {
    33. fail(service,
    34. "Provider " + cn + " could not be instantiated",
    35. x);
    36. }
    37. throw new Error(); // This cannot happen
    38. }

    三、JDK-SPI缺点

    1、需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。

    2、配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们

    3、扩展如果依赖其他的扩展,做不到自动注入和装配

    4、不提供类似于Spring的IOC和AOP功能

    5、扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的JDK SPI不支持

    四、SPI 和 API 区别

    API

    API(Application Programming Interface)的主要作用在于为调用方提供某个功能实现的调用入口,调用方不需关心该API的实现方式如何,它只需知道API可以提供特定的服务功能即可,具体实现由实现方负责。API提供特定的功能接口,以供调用;一般被应用开发人员使用,由实现方实现

    SPI

    SPI(Service Provider Interface)主要作用在于为同一服务的提供不同实现的替换机制,服务可以由第三方提供实现,也可以由调用方提供实现。SPI提供特定的功能接口,以供实现;一般被框架开发人员使用,调用方与实现方都可提供SPI的实现

  • 相关阅读:
    计算机网络:运输层 - TCP 流量控制 & 拥塞控制
    基于springboot的二手物品交易管理系统
    【LeetCode】63. 不同路径 II
    C# OpenCvSharp 图像处理函数-颜色通道-cvtColor
    Matlab图像处理-均值滤波,中值滤波和高斯滤波。
    【伪彩色图像处理】将灰度图像转换为彩色图像研究(Matlab代码实现)
    Python学习从0开始——项目一day02数据库连接
    AYIT嵌入式实验室2023级C语言训练1-4章训练题
    上海市青少年算法2023年8月月赛(丙组)
    c语言打印菱形图案
  • 原文地址:https://blog.csdn.net/MinggeQingchun/article/details/125768259