java程序员必须理解的SPI机制

java程序员必须理解的SPI机制

一、什么是SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

整体机制图如下:

在这里插入图片描述

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦

二、使用介绍

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

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

示例:

首先,我们需要定义一个接口,SPIService

package com.tzh.spi;

public interface SPIService {
    
    
    void execute();
}

然后,定义两个实现类

package com.tzh.spi.impl;

import com.tzh.spi.SPIService;

public class SpiImplOne implements SPIService {
    
    
    @Override
    public void execute() {
    
    
        System.out.println("One");
    }
}
package com.tzh.spi.impl;

import com.tzh.spi.SPIService;

public class SpiImplTwo implements SPIService {
    
    
    @Override
    public void execute() {
    
    
        System.out.println("Two");
    }
}

最后呢,要在ClassPath路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。
文件路径如下:

 /src/main/resources/META-INF/services/

SPI配置文件名称就是接口的全限定名称

com.tzh.spi.SPIService

内容就是实现类的全限定类名:

com.tzh.spi.impl.SpiImplOne
com.tzh.spi.impl.SpiImplTwo

测试:

然后我们就可以通过ServiceLoader.load方法拿到实现类的实例。

	@Test
    public void testExecute() {
    
    
        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);
        Iterator<SPIService> iterator = load.iterator();
        while (iterator.hasNext()) {
    
    
            SPIService ser = iterator.next();
            ser.execute();
        }
    }

方式的输出结果是一致的:

在这里插入图片描述

三、源码剖析

首先看ServiceLoader类的签名类的成员变量

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;
    // 缓存已加载的服务类集合providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //是其内部使用的迭代器,用于类的懒加载,只有在迭代时加载。
    private LazyIterator lookupIterator;
}

构造方法是一个private方法,不对外提供,在使用时我们需要调用其静态的load方法,由其自身产生ServiceLoader对象:

	//调用load方法
	public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
	
	public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    
    
        return new ServiceLoader<>(service, loader);
    }

可以看到对load方法进行了重载,其中参数service是要加载的类;单参方法没有类加载器,使用的是当前线程的类加载器;最后调用的是双参的load方法;而双参的load方法也很简单,只是直接调用ServiceLoader的构造方法,实例化了一个对象。

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    
    
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

可以看到其构造方法逻辑依旧很简单,首先是判断传入的svc(即传入的service)是否为空,若是为空直接报异常,否则给service 成员赋值,然后给进行cl的非空判断,给loader 成员赋值;接着给acc 成员赋值,其根据是否设置了安全管理器SecurityManager来赋值;最后调用reload方法。

public void reload() {
    
    
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

可以看到reload方法是一个public方法,那么在每次调用reload时就需要将之前加载的清空掉,所以直接使用providers这个map的clear方法清空掉缓存;接着使用刚才赋值后的service和loader产生一个LazyIterator对象赋值给lookupIterator成员。

LazyIterator是ServiceLoader的内部类,其定义如下:

private class LazyIterator implements Iterator<S> {
    
    
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;//服务的URL枚举
        Iterator<String> pending = null;//保存要加载的服务的名称集合
        String nextName = null;			//下一个要加载的服务名称

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

这里就可以看到ServiceLoader的实际加载过程就交给了LazyIterator来做,将ServiceLoader的service和loader成员分别赋值给了LazyIterator的service和loader成员。

ServiceLoader实现了Iterable接口,其实现的iterator方法如下:

public Iterator<S> iterator() {
    
    
        return new Iterator<S>() {
    
    

            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
    
    
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }

            public S next() {
    
    
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

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

        };
    }

可以看到它是直接创建了一个Iterator对象返回;其knownProviders成员直接获取providers的entrySet集合的迭代器;在hasNext和next方法中我们可以看到,它是先通过判断knownProviders里有没有(即providers),若没有再去lookupIterator中找;
前面我们可以看到providers里并没用put任何东西,那么就说明put操作也是在lookupIterator中完成的。

先看到lookupIterator的next方法:

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);
    }
}

首先根据判断acc是否为空,若为空则说明没有设置安全策略直接调用nextService方法,否则以特权方式调用nextService方法。

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
}	

首先根据hasNextService方法判断,若为false直接抛出NoSuchElementException异常,否则继续执行。

hasNextService方法:

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;
}

hasNextService方法首先根据nextName成员是否为空判断,若不为空,则说明已经初始化过了,直接返回true,否则继续执行。接着configs成员是否为空,configs 是一个URL的枚举,若是configs 没有初始化,就需要对configs初始化。
configs初始化逻辑也很简单,首先根据PREFIX前缀加上PREFIX的全名得到完整路径,再根据loader的有无,获取URL的枚举。其中fail方法时ServiceLoader的静态方法,用于异常的处理,后面给出。
在configs初始化完成后,还需要完成pending的初始化或者添加。
可以看到只有当pending为null,或者没有元素时才进行循环。循环时若是configs里没有元素,则直接返回false;否则调用ServiceLoader的parse方法,通过service和URL给pending赋值;

parse方法:

private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError{
    
    
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> 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();
}

可以看到parse方法直接通过URL打开输入流,通过parseLine一行一行地读取将结果保存在names数组里。

parseLine方法:

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,List<String> 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;
}

parseLine方法就是读该URL对应地文件地一行,可以看到通过对“#”的位置判断,忽略注释,并且剔除空格,接着是一系列的参数合法检验,然后判断providers和names里是否都没包含这个服务名称,若都没包含names直接add,最后返回下一行的行标;

当parse将所有内容读取完毕,返回names.iterator()赋值给hasNextService中的pending。循环结束,获取pending中的第一个元素赋值给nextName,返回true,hasNextService方法结束。

在nextService方法往下执行时,先用cn保存nextName的值,再让nextName=null,为下一次的遍历做准备;接着通过类加载,加载名为cn的类,再通过该类实例化对象,并用providers缓存起来,最后返回该实例对象。

其中cast方法是判断对象是否合法:

public T cast(Object obj) {
    
    
    if (obj != null && !isInstance(obj))
        throw new ClassCastException(cannotCastMsg(obj));
    return (T) obj;
}

至此ServiceLoader的迭代器的next方法结束。其hasNext方法与其类似,就不详细分析了。

而其remove方法就更直接,直接抛出异常来避免可能出现的危险情况:

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

ServiceLoader除了load的两个方法外还有个loadInstalled方法:

public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    
    
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
    
    
        prev = cl;
        cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
}

该方法与load方法不同在于loadInstalled使用的是扩展类加载器,而load使用的是传入进来的或者是线程的上下文类加载器,其他都一样。

四、应用场景

1、JDBC加载不同类型的驱动
2、SLF4J对log4j/logback的支持
3、Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
4、Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对java提供的原生SPI做了封装

--------------最后感谢大家的阅读,愿大家技术越来越流弊!--------------

在这里插入图片描述

--------------也希望大家给我点支持,谢谢各位大佬了!!!--------------

猜你喜欢

转载自blog.csdn.net/Zack_tzh/article/details/109778732