• SPI:Java的高可扩展利器


    摘要:JAVA SPI,基于接口的编程+策略模式+配置文件的动态加载机制。

    本文分享自华为云社区《一文讲透Java核心技术之高可扩展利器SPI》,作者: 冰 河。

    SPI的概念

    JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制

    SPI的使用场景

    Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。

    大量的框架使用了Java的SPI技术,如下:

    (1)JDBC加载不同类型的数据库驱动
    (2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
    (3)Spring中大量使用了SPI

    • 对servlet3.0规范
    • 对ServletContainerInitializer的实现
    • 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

    (4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现

    SPI的使用

    当服务的提供者,提供了接口的一种实现后,需要在Jar包的**META-INF/services/**目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名)。

    当外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该Jar包的**META/Services/**目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。

    SPI中查找接口的实现类是通过java.util.ServiceLoader,而在java.util.ServiceLoader类中有一行代码如下:

    // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    private static final String PREFIX = "META-INF/services/";

    这也就是说,我们必须将接口的配置文件写到Jar包的**META/Services/**目录下。

    SPI实例

    这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。

    注意:实例是基于Java8进行开发的。

    1.创建Maven项目

    在IDEA中创建Maven项目spi-demo,如下:

    2.编辑pom.xml

    复制代码
    "1.0" encoding="UTF-8"?>
    "http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    spi-demo
    io.binghe.spi
     jar
     1.0.0-SNAPSHOT
     4.0.0
     
     
     
     org.springframework.boot
     spring-boot-maven-plugin
     
     
     org.apache.maven.plugins
     maven-compiler-plugin
     3.6.0
     
     1.8
     1.8
     
     
     
     
    
    复制代码

    3.创建类加载工具类

    在io.binghe.spi.loader包下创建MyServiceLoader,MyServiceLoader类中直接调用JDK的ServiceLoader类加载Class。代码如下所示。

    复制代码
    package io.binghe.spi.loader;
    import java.util.ServiceLoader;
    /**
     * @author binghe
     * @version 1.0.0
     * @description 类加载工具
     */
    public class MyServiceLoader {
     /**
         * 使用SPI机制加载所有的Class
         */
     public static  ServiceLoader loadAll(final Class clazz) {
     return ServiceLoader.load(clazz);
     }
    }
    复制代码

    4.创建接口

    在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示:

    复制代码
    package io.binghe.spi.service;
    /**
     * @author binghe
     * @version 1.0.0
     * @description 定义接口
     */
    public interface MyService {
     /**
         *  打印信息
         */
     void print(String info);
    }
    复制代码

    5.创建接口的实现类

    (1)创建第一个实现类MyServiceA

    在io.binghe.spi.service.impl包下创建MyServiceA类,实现MyService接口。代码如下所示:

    复制代码
    package io.binghe.spi.service.impl;
    import io.binghe.spi.service.MyService;
    /**
     * @author binghe
     * @version 1.0.0
     * @description 接口的第一个实现
     */
    public class MyServiceA implements MyService {
     @Override
     public void print(String info) {
     System.out.println(MyServiceA.class.getName() + " print " + info);
     }
    }
    复制代码

    (2)创建第二个实现类MyServiceB

    在io.binghe.spi.service.impl包下创建MyServiceB类,实现MyService接口。代码如下所示:

    复制代码
    package io.binghe.spi.service.impl;
    import io.binghe.spi.service.MyService;
    /**
     * @author binghe
     * @version 1.0.0
     * @description 接口第二个实现
     */
    public class MyServiceB implements MyService {
     @Override
     public void print(String info) {
     System.out.println(MyServiceB.class.getName() + " print " + info);
     }
    }
    复制代码

    6.创建接口文件

    在项目的src/main/resources目录下创建**META/Services/**目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示:

    io.binghe.spi.service.impl.MyServiceA
    io.binghe.spi.service.impl.MyServiceB

    7.创建测试类

    在项目的io.binghe.spi.main包下创建Main类,该类为测试程序的入口类,提供一个main()方法,在main()方法中调用ServiceLoader类加载MyService接口的实现类。并通过Java8的Stream将结果打印出来,如下所示:

    复制代码
    package io.binghe.spi.main;
    import io.binghe.spi.loader.MyServiceLoader;
    import io.binghe.spi.service.MyService;
    import java.util.ServiceLoader;
    import java.util.stream.StreamSupport;
    /**
     * @author binghe
     * @version 1.0.0
     * @description 测试的main方法
     */
    public class Main {
     public static void main(String[] args){
     ServiceLoader loader = MyServiceLoader.loadAll(MyService.class);
     StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
     }
    }
    复制代码

    8.测试实例

    运行Main类中的main()方法,打印出的信息如下所示:

    io.binghe.spi.service.impl.MyServiceA print Hello World
    io.binghe.spi.service.impl.MyServiceB print Hello World
    Process finished with exit code 0

    通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法。

    源码解析

    这里,主要是对SPI的加载流程涉及到的java.util.ServiceLoader的源码的解析。

    进入java.util.ServiceLoader的源码,可以看到ServiceLoader类实现了java.lang.Iterable接口,如下所示。

    public final class ServiceLoader implements Iterable 

    说明ServiceLoader类是可以遍历迭代的。

    java.util.ServiceLoader类中定义了如下的成员变量:

    复制代码
    // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
    private static final String PREFIX = "META-INF/services/";
    // 需要加载的接口
    private final Class service;
    // 类加载器,用于加载以接口命名的文件中配置的接口的实现类
    private final ClassLoader loader;
    // 创建ServiceLoader时采用的访问控制上下文环境
    private final AccessControlContext acc;
    // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
    private LinkedHashMap providers = new LinkedHashMap<>();
    // 用于延迟加载实现类的迭代器
    private LazyIterator lookupIterator;
    复制代码

    可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的src/main/resources目录下的**META-INF/services/**目录下创建。

    从MyServiceLoader类调用**ServiceLoader.load(clazz)**方法进入源码,如下所示:

    复制代码
    //根据类的Class对象加载指定的类,返回ServiceLoader对象
    public static  ServiceLoader load(Class service) {
    //获取当前线程的类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    //动态加载指定的类,将类加载到ServiceLoader中
    return ServiceLoader.load(service, cl);
    }
    复制代码

    方法中调用了**ServiceLoader.load(service, cl)**方法,继续跟踪代码,如下所示:

    //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
    public static  ServiceLoader load(Class service, ClassLoader loader){
    return new ServiceLoader<>(service, loader);
    }

    可以看到**ServiceLoader.load(service, cl)**方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示:

    复制代码
    //构造ServiceLoader对象
    private ServiceLoader(Class svc, ClassLoader cl) {
    //如果传入的Class对象为空,则判处空指针异常
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
    }
    复制代码

    继续跟**reload()**方法,如下所示。

    复制代码
    //重新加载
    public void reload() {
    //清空保存加载的实现类的LinkedHashMap
    providers.clear();
    //构造延迟加载的迭代器
    lookupIterator = new LazyIterator(service, loader);
    }
    复制代码

    继续跟进懒加载迭代器的构造函数,如下所示。

    private LazyIterator(Class service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
    }

    可以看到,会将需要加载的接口的Class对象和类加载器赋值给LazyIterator的成员变量。

    当我们在程序中迭代获取对象实例时,首先在成员变量**providers中查找是否有缓存的实例对象。如果存在则直接返回,否则调用lookupIterator**延迟加载迭代器进行加载。

    迭代器进行逻辑判断的代码如下所示:

    复制代码
    //迭代ServiceLoader的方法
    public Iterator iterator() {
    return new Iterator() {
    //获取保存实现类的LinkedHashMap的迭代器
    Iterator> knownProviders = providers.entrySet().iterator();
    //判断是否有下一个元素
    public boolean hasNext() {
    //如果knownProviders存在元素,则直接返回true
    if (knownProviders.hasNext())
    return true;
    //返回延迟加载器是否存在元素
    return lookupIterator.hasNext();
    }
    //获取下一个元素
    public S next() {
    //如果knownProviders存在元素,则直接获取
    if (knownProviders.hasNext())
    return knownProviders.next().getValue();
    //获取延迟迭代器lookupIterator中的元素
    return lookupIterator.next();
    }
    public void remove() {
    throw new UnsupportedOperationException();
    }
    };
    }
    复制代码

    LazyIterator加载类的流程如下代码所示

    复制代码
    //判断是否拥有下一个实例
    private boolean hasNextService() {
    //如果拥有下一个实例,直接返回true
    if (nextName != null) {
    return true;
    }
    //如果实现类的全名为null
    if (configs == null) {
    try {
    //获取全文件名,文件相对路径+文件名称(包名+接口名)
    String fullName = PREFIX + service.getName();
    //类加载器为空,则通过ClassLoader.getSystemResources()方法获取
    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()) {
    //如果configs中没有更过的元素,则直接返回false
    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 {
    //通过c.newInstance()生成对象实例
    S p = service.cast(c.newInstance());
    //将生成的对象实例保存到缓存中(LinkedHashMap
    providers.put(cn, p);
    return p;
    } catch (Throwable x) {
    fail(service,
     "Provider " + cn + " could not be instantiated",
     x);
    }
    throw new Error(); // This cannot happen
    }
    public boolean hasNext() {
    if (acc == null) {
    return hasNextService();
    } else {
    PrivilegedAction action = new PrivilegedAction() {
    public Boolean run() { return hasNextService(); }
    };
    return AccessController.doPrivileged(action, acc);
    }
    }
    public S next() {
    if (acc == null) {
    return nextService();
    } else {
    PrivilegedAction action = new PrivilegedAction() {
    public S run() { return nextService(); }
    };
    return AccessController.doPrivileged(action, acc);
    }
    }
    复制代码

    最后,给出整个java.util.ServiceLoader的类,如下所示:

    复制代码
    package java.util;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.URL;
    import java.security.AccessControlContext;
    import java.security.AccessController;
    import java.security.PrivilegedAction;
    public final class ServiceLoader implements Iterable {
     // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
     private static final String PREFIX = "META-INF/services/";
     // 需要加载的接口
     private final Class service;
     // 类加载器,用于加载以接口命名的文件中配置的接口的实现类
     private final ClassLoader loader;
     // 创建ServiceLoader时采用的访问控制上下文环境
     private final AccessControlContext acc;
     // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
     private LinkedHashMap providers = new LinkedHashMap<>();
     // 用于延迟加载实现类的迭代器
     private LazyIterator lookupIterator;
     //重新加载
     public void reload() {
     //清空保存加载的实现类的LinkedHashMap
     providers.clear();
     //构造延迟加载的迭代器
     lookupIterator = new LazyIterator(service, loader);
     }
     //构造ServiceLoader对象
     private ServiceLoader(Class svc, ClassLoader cl) {
     //如果传入的Class对象为空,则判处空指针异常
            service = Objects.requireNonNull(svc, "Service interface cannot be null");
     //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
            loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
            acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
     reload();
     }
     private static void fail(Class service, String msg, Throwable cause)
     throws ServiceConfigurationError
     {
     throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                                cause);
     }
     private static void fail(Class service, String msg)
     throws ServiceConfigurationError
     {
     throw new ServiceConfigurationError(service.getName() + ": " + msg);
     }
     private static void fail(Class service, URL u, int line, String msg)
     throws ServiceConfigurationError
     {
     fail(service, u + ":" + line + ": " + msg);
     }
     // Parse a single line from the given configuration file, adding the name
     // on the line to the names list.
     //
     private int parseLine(Class service, URL u, BufferedReader r, int lc,
     List names)
     throws IOException, ServiceConfigurationError
     {
     String ln = r.readLine();
     if (ln == null) {
     return -1;
     }
     int ci = ln.indexOf('#');
     if (ci >= 0) ln = ln.substring(0, ci);
            ln = ln.trim();
     int n = ln.length();
     if (n != 0) {
     if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
     fail(service, u, lc, "Illegal configuration-file syntax");
     int cp = ln.codePointAt(0);
     if (!Character.isJavaIdentifierStart(cp))
     fail(service, u, lc, "Illegal provider-class name: " + ln);
     for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                    cp = ln.codePointAt(i);
     if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
     fail(service, u, lc, "Illegal provider-class name: " + ln);
     }
     if (!providers.containsKey(ln) && !names.contains(ln))
     names.add(ln);
     }
     return lc + 1;
     }
     private Iterator parse(Class service, URL u)
     throws ServiceConfigurationError
     {
     InputStream in = null;
     BufferedReader r = null;
     ArrayList names = new ArrayList<>();
     try {
                in = u.openStream();
                r = new BufferedReader(new InputStreamReader(in, "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 inner class implementing fully-lazy provider lookupload
     private class LazyIterator
     implements Iterator
     {
     Class service;
     ClassLoader loader;
     Enumeration configs = null;
     Iterator pending = null;
     String nextName = null;
     private LazyIterator(Class service, ClassLoader loader) {
     this.service = service;
     this.loader = loader;
     }
     //判断是否拥有下一个实例
     private boolean hasNextService() {
     //如果拥有下一个实例,直接返回true
     if (nextName != null) {
     return true;
     }
     //如果实现类的全名为null
     if (configs == null) {
     try {
     //获取全文件名,文件相对路径+文件名称(包名+接口名)
     String fullName = PREFIX + service.getName();
     //类加载器为空,则通过ClassLoader.getSystemResources()方法获取
     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()) {
     //如果configs中没有更过的元素,则直接返回false
     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 {
     //通过c.newInstance()生成对象实例
     S p = service.cast(c.newInstance());
     //将生成的对象实例保存到缓存中(LinkedHashMap
     providers.put(cn, p);
     return p;
     } catch (Throwable x) {
     fail(service,
     "Provider " + cn + " could not be instantiated",
                         x);
     }
     throw new Error(); // This cannot happen
     }
     public boolean hasNext() {
     if (acc == null) {
     return hasNextService();
     } else {
     PrivilegedAction action = new PrivilegedAction() {
     public Boolean run() { return hasNextService(); }
     };
     return AccessController.doPrivileged(action, acc);
     }
     }
     public S next() {
     if (acc == null) {
     return nextService();
     } else {
     PrivilegedAction action = new PrivilegedAction() {
     public S run() { return nextService(); }
     };
     return AccessController.doPrivileged(action, acc);
     }
     }
     public void remove() {
     throw new UnsupportedOperationException();
     }
     }
     //迭代ServiceLoader的方法
     public Iterator iterator() {
     return new Iterator() {
     //获取保存实现类的LinkedHashMap的迭代器
     Iterator> knownProviders = providers.entrySet().iterator();
     //判断是否有下一个元素
     public boolean hasNext() {
     //如果knownProviders存在元素,则直接返回true
     if (knownProviders.hasNext())
     return true;
     //返回延迟加载器是否存在元素
     return lookupIterator.hasNext();
     }
     //获取下一个元素
     public S next() {
     //如果knownProviders存在元素,则直接获取
     if (knownProviders.hasNext())
     return knownProviders.next().getValue();
     //获取延迟迭代器lookupIterator中的元素
     return lookupIterator.next();
     }
     public void remove() {
     throw new UnsupportedOperationException();
     }
     };
     }
     //通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
     public static  ServiceLoader load(Class service,
     ClassLoader loader)
     {
     return new ServiceLoader<>(service, loader);
     }
     //根据类的Class对象加载指定的类,返回ServiceLoader对象
     public static  ServiceLoader load(Class service) {
     //获取当前线程的类加载器
     ClassLoader cl = Thread.currentThread().getContextClassLoader();
     //动态加载指定的类,将类加载到ServiceLoader中
     return ServiceLoader.load(service, cl);
     }
     public static  ServiceLoader loadInstalled(Class service) {
     ClassLoader cl = ClassLoader.getSystemClassLoader();
     ClassLoader prev = null;
     while (cl != null) {
     prev = cl;
                cl = cl.getParent();
     }
     return ServiceLoader.load(service, prev);
     }
     /**
         * Returns a string describing this service.
         *
         * @return  A descriptive string
         */
     public String toString() {
     return "java.util.ServiceLoader[" + service.getName() + "]";
     }
    }
    复制代码

    SPI总结

    优点:

    能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

    缺点:

    • 多个并发多线程使用ServiceLoader类的实例是不安全的
    • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

    参考:深入理解Java中的spi机制

     

    点击关注,第一时间了解华为云新鲜技术~

  • 相关阅读:
    安卓打开第三方应用失败
    英伟达发布526.47驱动,可支持新款RTX 3060/3060 Ti显卡
    心法利器[77] | 文本分类日常提点技巧
    基于C#和OpenVINO在英特尔独立显卡上部署PP-TinyPose模型
    Transformers are RNNs (linear transformer)论文阅读
    设置共享文件夹在主机与本地VMware虚拟机之间传输文件
    J2EE——自定义MVC框架的CRUD操作
    superset-1.5.0 docker版连接Oracle、SQL server、Elasticsearch
    使用JS代理 实现大对象的功能拆解
    【测试面试】软件测试工程师面试踩过的坑(实战经验分享)
  • 原文地址:https://www.cnblogs.com/huaweiyun/p/16591831.html