你了解JDK SPI吗?

286 阅读6分钟

什么是JDK SPI 机制?

SPI (Service Provider Interface) 是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔。

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

为什么要引入JDK SPI?

上述已经描述清楚,在Java中SPI是被用来设计给服务提供商做插件用的,基于策略模式,来实现动态加载机制。 例如我们在程序中只定义一个接口(规范),而具体的实现交给不同的服务提供者,在程序的启动中,读取设置好的配置文件,由配置文件决定加载哪一种服务的实现。 这种将服务接口与实现分离达到解耦的作用,同时也提升了程序的可扩展性

举例说明: 使用Java语言访问数据库的时候我们会使用到Java.sql.Driver接口 不同数据库产品底层的协议不同,提供的Driver实现也不同,而开发者也不确定用户会选择哪种数据库,所以这种情况下可以使用JDK SPI机制,根据配置文件 用户选择哪种数据库,就调用哪种数据库的实现。

简单实现

写一个简单的例子 有一个接口Log,定义一个方法log,作用:作为日志输出传入参数。 而实现接口的方式有两种,分别是Log4j和Logback。 具体要使用哪种方式看配置文件。 就这样,一个小例子

下面是代码

在这里插入图片描述

Log接口

public interface Log {
    void log(String info);
}

两个实现类

public class Logback implements Log {

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

public class Log4j implements Log {

    @Override
    public void log(String info) {
        System.out.println("Log4jLog4j:" + info);
    }
}

service配置文件

service.impl.Log4j
service.impl.Logback

测试类

public class Main {

    public static void main(String[] args) throws IOException {
        ServiceLoader<Log> spiLoader = ServiceLoader.load(Log.class);
        Iterator<Log> iteratorSpi = spiLoader.iterator();
        while (iteratorSpi.hasNext()) {
            Log log = iteratorSpi.next();
            log.log("MRyan");
        }
    }
}

输出: 在这里插入图片描述

源码分析

由 ServiceLoader的load方法作为入口进行分析 传入的参数是接口类

   public static <S> ServiceLoader<S> load(Class<S> service) {
   		//获取当前线程绑定的ClassLoader
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

接着调用重载load()传入接口类和,当前线程绑定的ClassLoader

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader){
  		  //为给定的服务类型和类加载器创建一个新的服务加载器。
        return new ServiceLoader<>(service, loader);
    }
   // 表示正在加载的服务的类或接口
    private final Class<S> service;

    // 用于查找,加载和实例化提供程序的类加载器
    private final ClassLoader loader;

    // 创建ServiceLoader时采取的访问控制上下文
    private final AccessControlContext acc;

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
    	//判断当前接口类引用返回是否为空
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //判断当前线程绑定的ClassLoader是否为空  为空则调用SystemClassLoader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //判断AccessControlContext是否为空
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //清除此加载程序的提供程序缓存,重新加载所有提供程序
        reload();
    }

核心方法reload,来看看它都做了什么

    // provider的缓存
    //该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    //懒加载迭代器
    private LazyIterator lookupIterator;

	//清除此加载程序的提供程序缓存,重新加载所有提供程序
  public void reload() {
  		//清空provider的缓存  
        providers.clear();
        //创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。
        lookupIterator = new LazyIterator(service, loader);
    }

有点好奇LazyIterator是什么,看名字像是迭代器,有点眼熟,我们发现测试类中迭代器就是调用 ServiceLoader.LazyIterator 实现的。 Iterator 接口有两个关键方法:hasNext() 方法和 next() 方法。 这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法, hasNext() 方法最终调用的是 hasNextService() 方法

	private static final String PREFIX = "META-INF/services/"; 
	Enumeration<URL> configs = null; 
	Iterator<String> pending = null; 
	String nextName = null; 

	 private class LazyIterator
        implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;

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

        private boolean hasNextService() {
            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;
        }

        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
        }

        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    }

其中着重看下hasNextService方法

  private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                //很熟悉有没有 PREFIX 赋值是 "META-INF/services/"
                    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);
                }
            }
            // 按行SPI遍历配置文件的内容 
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                  // 解析配置文件 
                pending = parse(service, configs.nextElement());
            }
            //更新字段
            nextName = pending.next();
            return true;
        }

在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看nextService() 方法,该方法负责实例化 hasNextService() 方法读取到的实现类,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示:

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            // // 加载 nextName字段指定的类 
                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
        }

说了这么多迭代器的实现,那么测试类中的迭代是怎么实现的?

public Iterator<S> iterator() {
        return new Iterator<S>() {
			// knownProviders用来迭代providers缓存 
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
			
			// 先走查询缓存,缓存查询失败,再通过LazyIterator加载
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

			//// 先走查询缓存,缓存查询失败,再通过 LazyIterator加载 
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }

到这里,文章就接近尾声了,了解了SPI是什么,SPI能做什么,SPI的实现,那么问题来了,

他的缺点是什么呢

其实很明显真正实现是需要读取配置类的信息,那我们如果需要新增一个驱动类的话,是不是需要在配置类中手动的添加一行,那删除一个驱动类的时候也需要手动删除,那这样的话扩展性是不是就不好了呢。

如果你喜欢这篇文章的话,不妨动动手指关注我哦。