小窝

探究 SPI 机制原理及优缺点

发布时间:5年前作者:shine热度: 1975 ℃评论数:

一、SPI概念

         SPI ,全称Service Provider Interface,从字面理解SPI 就是 Service 提供者接口是一种服务发现机制。它通过查找 ClassPath 路径下的 META-INF/services 文件夹中的文件,自动加载文件里所定义的类。

 

       SPI 机制在 Dubbo、common-logging、JDBC等框架中都有应用,是厂商为开发者预留的扩展框架能力的的接口,为框架扩展提供了可能

 

       在日常开发中都是将问题抽象成各种 API 接口,然后提供各种实现,这些API接口的实现都是封装在我们的 Jar 或者框架中,当我们想提供一种新的实现时可以不修改原来的代码只需实现API 接口就可以了 但我们还是生成新 Jar 或框架(虽然可以通过在代码里扫描某个目录已加载Api的新实现,但这不是 Java 的机制,只是hack方法),通过Java SPI 机制我们可以在不修改 Jar 或者框架的时候为 API 接口提供新实现

      1. 小例子

      1.1 先定义一个需要实现的接口,如下

package com.myzuji.study.spi;

public interface SPIService {

void execute(); }

      1.2 在定义两个实现,如下

package com.myzuji.study.spi.impl;

import com.myzuji.study.spi.SPIService;

public class SPIServiceImpl1 implements SPIService {

    @Override

    public void execute() {

       System.out.println("SPIImpl1.excute()");

    }

}

package com.myzuji.study.spi.impl;

import com.myzuji.study.spi.SPIService;


public class SPIServiceImpl2 implements SPIService {

    @Override

    public void execute() {

       System.out.println("SPIImpl2.excute()");

    }

}

      1.3 SPI 配置,如下

         在ClassPath路径下的META-INF/services文件夹下创建一个文件,文件名为接口的全限定名com.myzuji.study.spi.SPIService,文件内容为实现类的全限定名,多个实现类用换行符分隔,如下

com.myzuji.study.spi.impl.SPIServiceImpl1
com.myzuji.study.spi.impl.SPIServiceImpl2

      2. 测试

         然后我们就可以通过ServiceLoader.load或者Service.providers方法拿到实现类的实例。其中,Service.providers包位于sun.misc.Service,而ServiceLoader.load包位于java.util.ServiceLoader。代码如下:

package com.myzuji.study.spi;
		
import sun.misc.Service;

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

public class SPITest {

    public static void main(String[] args) {
        Iterator providers = Service.providers(SPIService.class);
        while (providers.hasNext()) {
            SPIService spiService = providers.next();
            spiService.execute();
        }
        System.out.println("============");
        ServiceLoader loader = ServiceLoader.load(SPIService.class);
        Iterator spiServiceIterator = loader.iterator();
        while (spiServiceIterator.hasNext()) {
            SPIService spiService = spiServiceIterator.next();
            spiService.execute();
        }
    }
}
输出结果是:
SPIImpl1.excute()
SPIImpl2.excute()
============
SPIImpl1.excute()
SPIImpl2.excute()

         小例子的目录结构

com/myzuji/study/spi/impl/SPIServiceImpl1.java
com/myzuji/study/spi/impl/SPIServiceImpl2.java
com/myzuji/study/spi/SPIService.java
com/myzuji/study/spi/SPITest.java
META-INF/services/com.myzuji.study.spi.SPIService

二、源码分析

         在运行测试方法时会发现有两种加载方式,分别位于 JDK 的 sun.misc 和 java.util 包中, sun 包下的 源码看不到,就以 ServiceLoader.load() 为例,通过源码看看它里面到底怎么做的。

         通过上面的测试用例可以发现在调用 ServiceLoader.load(SPIService.class) 时,进入 

public static  ServiceLoader load(Class service) {
    //获取当前线程上下文加载器
    ClassLoader cl=Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service,cl);
}

         继续进入 ServiceLoader.load(service,cl)

public static  ServiceLoader load(Class service,ClassLoader loader) {
    return new ServiceLoader<>(service, loader);
}

         返回一个 ServiceLoader 实例,继续看下它的构造函数

public final class ServiceLoader implements Iterable {
    
    //配文件路径
    private static final String PREFIX = "META-INF/services/";
    //代表被加载的服务类或接口
    private final Class service;
    //用于定位,加载和实例化的类加载器
    private final ClassLoader loader;
    
    //创建 ServiceLoader 时采用的访问控制上下文
    private final AccessControlContext acc;
    
    //已加载的服务类集合
    private LinkedHashMap providers = new LinkedHashMap<>();
    
    //内部类,真正加载服务的类
    private LazyIterator lookupIterator;
    
    public void reload(){
        //先清空
        providers.clear();
        //实例化内部类
        lookupIterator=newLazyIterator(service,loader);
    }
    
    private ServiceLoader(Class 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();
}

         在调用 Iterator<SPIService> spiServiceIterator=loader.iterator()时返回了一个 Iterator 容器

public Iterator iterator() {
    return new Iterator() {
        Iterator> knownProviders = providers.entrySet().iterator();

        public boolean hasNext() {
	//第一次执行 hasNext() knownProviders 的 size 为 0 会继续执行lookupIterator.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();
        }
    };
}

         进入lookupIterator.hasNext()  最终会进入到内部类 LazyIterator 的 hasNextService()

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

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

         当调用  spiServiceIterator.next()方法时,进入到内部类 LazyIteratornextService() 

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

private S nextService() {
	if (!hasNextService())
		throw new NoSuchElementException();
	String cn = nextName;
	nextName = null;
	Class c = null;
	try {
		//生成名称为 cn 的 Class 对象,不进行初始化
		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
}

         通过上面的分析可以看到 ServiceLoader 不是立即实例化服务实现者的,只有等到需要时才通过Iterator 实现获取对应的服务提供者才会加载对应的配置文件进行解析,具体来说是在调用Iterator的hasNext方法时会去加载配置文件进行解析,在调用next方法时会将对应的服务提供者进行实例化并进行缓存。所有的配置文件只加载一次,服务提供者也只实例化一次,如需要重新加载配置文件可调用ServiceLoader的reload方法。


三、 总结

优点:实现解耦,应用程序可以根据实现业务情况启用或替换组件

缺点:

> 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。

> 获取某个实现类的方式不够灵活,只能通过Iterator 形式获取,不能根据某个参数类获取对应的实现类。

多个并发多线程使用ServiceLoadder 类的实例不安全

> 加载不到实现类时抛出并不是真正原因的异常,错误难定位。


         鉴于SPI 的诸多缺点,很多系统都是自己实现一套类加载机制,如DUBBO

热门评论

手机扫码访问