美文网首页
JAVA中的SPI机制

JAVA中的SPI机制

作者: zfylin | 来源:发表于2020-09-11 17:00 被阅读0次

    介绍

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

    机制如图:

    image.png

    当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。

    JDK中查找服务的实现的工具类是:java.util.ServiceLoader

    Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,核心思想就是解耦

    使用场景

    SPI机制适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。比如Common-Logging,JDBC,Dubbo等等。

    要使用Java SPI,需要遵循如下约定:

    • 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
    • 接口实现类所在的jar包放在主程序的classpath中;
    • 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
    • SPI的实现类必须携带一个不带参数的构造方法;

    例子

    • 定义一个接口

      public interface ISearch {
          List<String> search(String keyword);
      }
      
    • 接口实现

      public class FileSearch implements ISearch {
          @Override
          public List<String> search(String keyword) {
              System.out.println("文件搜索 " + keyword);
              return null;
          }
      }
      
      public class DatabaseSearch implements ISearch {
          @Override
          public List<String> search(String keyword) {
              System.out.println("数据搜索 " + keyword);
              return null;
          }
      }
      
    • TestCase

      public class TestCase {
          public static void main(String[] args) {
              ServiceLoader<ISearch> s = ServiceLoader.load(ISearch.class);
              Iterator<ISearch> iterator = s.iterator();
              while (iterator.hasNext()) {
                  ISearch search =  iterator.next();
                  search.search("hello world");
              }
          }
      }
      
    • 配置META-INF/services

      Resource下面创建META-INF/services 目录里创建一个以服务接口命名的文件

    image.png

    文件内容如下:

    com.zfylin.java.learning.spi.FileSearch
    com.zfylin.java.learning.spi.DatabaseSearch
    
    • 运行TestCase,结果如下:

      文件搜索 hello world
      数据搜索 hello world
      

    源码分析

    // ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
    public final class ServiceLoader<S> implements Iterable<S>
    {
        // 查找配置文件的目录
        private static final String PREFIX = "META-INF/services/";
        // 要被加载的服务的类或接口
        private final Class<S> service;
        // 用于定位,加载和实例化providers的类加载器
        private final ClassLoader loader;
        // 创建ServiceLoader时采用的访问控制上下文
        private final AccessControlContext acc;
        // 缓存已经被实例化的服务提供者,按照实例化的顺序存储
        private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
        // 懒查找迭代器
        private LazyIterator lookupIterator; 
        
        ....
    }
    
    // 服务提供者查找的迭代器
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            // hasNext方法
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
            // next方法
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
        };
    }
    
    // 服务提供者查找的迭代器
    private class LazyIterator implements Iterator<S> {
        // 服务提供者接口
        Class<S> service;
        // 类加载器
        ClassLoader loader;
        // 保存实现类的url
        Enumeration<URL> configs = null;
        // 保存实现类的全名
        Iterator<String> pending = null;
        // 迭代器中下一个实现类的全名
        String nextName = null;
     
        public boolean hasNext() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    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;
        }
     
        public S next() {
            if (!hasNext()) {
                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, x);
            }
            throw new Error();          // This cannot happen
        }
    }
    

    首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。

    其次,LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。

    最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。

    参考

    深入理解 Java 中 SPI 机制

    相关文章

      网友评论

          本文标题:JAVA中的SPI机制

          本文链接:https://www.haomeiwen.com/subject/wmdbektx.html