java之spi详解

作者:zen

API与SPI

API (Application Programming Interface)是我们平常最常用的方式,由服务提供方来制定接口并完成对接口的实现,调用方仅仅依赖接口调用,具体的实现完全由服务提供方决定。

SPI (Service Provider Interface)是调用方来制定接口规范,而由外部来完成具体实现,调用方在调用时则选择自己需要的外部实现。

总体来说,Api是接口和实现都有服务提供者定义,而Spi则是调用方提供接口,服务提供者提供实现,调用方在调用的时候根据特定条件,选择自己需要的实现类。

ServiceLoader类加载说明

ServiceLoader是实现SPI一个重要的类。是在java1.6引入的类,为了解决接口与实现分离的场景。在资源目录META-INF/services中放置提供者配置文件,文件名以接口的类名命名,里面的内容为需要加载的实现类。然后在运行运行时,遇到Serviceloader.load(XxxInterface.class)时,会到META-INF/services的配置文件中寻找这个XxxInterface接口,全路径名命名的文件,文件中,定义类该接口具体的实现类,然后使用Class.forName()(传入设定的类加载器)完成类的加载。

ServiceLoader实现原理

以下是ServiceLoader实现Spi的主要代码

参考具体ServiceLoader具体源码,梳理了一下,实现的流程如下:

  1. 应用程序调用ServiceLoader.load方法
    ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,代码如下

         this.service = svc;
         this.serviceName = svc.getName();
         this.layer = null;
         this.loader = cl;
         this.acc = (System.getSecurityManager() != null)
                 ? AccessController.getContext()
                 : null;
    

    说明:

    • service(load的接口)
    • loader(ClassLoader类型,类加载器)
    • acc(AccessControlContext类型,访问控制器)
    • providers(LinkedHashMap类型,用于缓存加载成功的类)
    • lookupIterator(实现迭代器功能)
  2. 应用程序通过迭代器接口获取对象实例
    ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回。

            @Override
            public S next() {
                checkReloadCount();
                S next;
                if (index < instantiatedProviders.size()) {
                    next = instantiatedProviders.get(index);
                } else {
                    next = lookupIterator1.next().get();
                    instantiatedProviders.add(next);
                }
                index++;
                return next;
            }

如果没有缓存,执行类的装载,实现如下:

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

        /**
         * Loads and returns the next provider class.
         */
        private Class nextProviderClass() {
            if (configs == null) {
                try {
                    // 生成Spi配置文件路径
                    String fullName = PREFIX + service.getName();

                    // 通过类加载器加载该配置文件
                    if (loader == null) {
                        configs = ClassLoader.getSystemResources(fullName);
                    } else if (loader == ClassLoaders.platformClassLoader()) {
                        if (BootLoader.hasClassPath()) {
                            configs = BootLoader.findResources(fullName);
                        } else {
                            configs = Collections.emptyEnumeration();
                        }
                    } else {
                        configs = loader.getResources(fullName);
                    }
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }

                // 解析配置文件
                pending = parse(configs.nextElement());
            }
            String cn = pending.next();
            try {
                // 通过Class.forName的方式加载实现类
                return Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service, "Provider " + cn + " not found");
                return null;
            }
        }

说明如下:

  • (1) 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件
  • (2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
  • (3) 把实例化后的类缓存到providers对象中,(LinkedHashMap类型)然后返回实例对象。

Spi应用示例

我们现在手动完成一个Spi案例,来了解一下Spi如何应用,我们案例通过定义一个文件传输接口,然后实现类通过不同的协议来完成文件的传输
定义接口File:

package com.zen.spi;

public interface File {
    void read();
}

以下是两个实现类:

package com.zen.spi;

public class HttpFile implements File {

    @Override
    public void read() {
        System.out.println("read file by http");
    }
}
package com.zen.spi;

public class FtfFile implements File {

    @Override
    public void read() {
        System.out.println("read file by ftp");
    }
}

接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.zen.spi.File,里面加上我们需要用到的实现类

META-INF/services/com.zen.spi.File

com.zen.spi.HttpFile
com.zen.spi.FtpFile

然后写一个测试方法

package com.zen.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader loads = ServiceLoader.load(File.class);
        Iterator iterator = loads.iterator();
        while (iterator.hasNext()){
            File next = iterator.next();
            next.read();
        }
    }
}

输出结果:

read file by http
read file by ftp

Spi在Jdbc中的应用

JDBC4.0之后,JDBC中获取Mysql数据库驱动类不再需要通过Class.forName(“com.mysql.jdbc.Driver”)加载数据库相关的驱动,而是可以通过Spi来直接实现。
我们打开mysql-connector-java.jar包看下:


image.png

java.sql.Driver内容如下:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

定义了Driver的的实现类

我们在获取数据库连接的代码如下:

         String url="jdbc:mysql://localhost:3306/spi";
        Connection root = DriverManager.getConnection(url, "root", "root");

我们跟到源码中发现有如下代码:

         AccessController.doPrivileged(new PrivilegedAction() {
                public Void run() {

                    // 通过ServiceLoader来加载Driver的实现类
                    ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator driversIterator = loadedDrivers.iterator();

                    try {
                        while (driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch (Throwable t) {
                        // Do nothing
                    }
                    return null;
                }
            });

内部实现也是通过通过ServiceLoader来加载Driver的实现类的方式来实现,具体实现的流程已经在ServiceLoader实现原理中看到了。

总结

优点:

  • 使用Java SPI机制的优势是实现解耦,在服务调用方定义好业务接口,而具体的逻辑实现则由服务提供方完成。使用者不用手动引入实现类,也不需要关系实现类的具体位置。提供者只需要按照Spi规范定义好具体实现即可。

缺点:

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

你可能感兴趣的:(java之spi详解)