SPI
全名为Service Provider Interface
是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件,简单来说,它就是一种动态替换发现的机制。
在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
jar
包的META-INF/services/
目录里同时创建一个以服务接口/抽象类的全限定名命名的文件。内容为实现该服务接口的具体实现类的全限类名。为什么一定要在classes中的META-INF/services下呢?
因为JDK提供服务实现查找的一个工具类:java.util.ServiceLoader默认指定从这个路径下查找实现类
概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
比较常见的例子:
新建工程,层级结构如下:
具体代码实现:
//SPI接口
public interface SpiService {
void study();
}
//SPI接口实现1
public class JimSpiServiceImpl implements SpiService {
@Override
public void study() {
System.out.println("学习mysql");
}
}
//SPI接口实现2
public class TomSpiServiceImpl implements SpiService{
@Override
public void study() {
System.out.println("学习java");
}
}
//接口权限名的文件内容
com.pingan.haofang.demo.JimSpiServiceImpl
com.pingan.haofang.demo.TomSpiServiceImpl
//测试类
public class SpiTest {
public static void main(String[] args) {
ServiceLoader services = ServiceLoader.load(SpiService.class);
for (SpiService spiService : services) {
spiService.study();
}
}
}
//运行结果:
学习mysql
学习java
1 、应用程序调用ServiceLoader.load方法去先创建一个新的ServiceLoader,并实例化该类中的成员变量
public final class ServiceLoader
implements Iterable
{
// 加载具体实现类信息的前缀
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private Class service;
// 用于定位,加载和实例化providers的类加载器
private ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 用于缓存已经加载的接口实现类,其中key为实现类的完整类名
private LinkedHashMap providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
serviceLoader的load方法
public static ServiceLoader load(Class service) {
//使用的是线程上下文类加载器(不遵循双亲委派)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static ServiceLoader load(Class service,
ClassLoader loader)
{
return new ServiceLoader<>(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();
}
public void reload() {
providers.clear();// 清空已经缓存的加载的接口实现类
lookupIterator = new LazyIterator(service, loader);// 创建新的延迟加载迭代器
}
private LazyIterator(Class service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
2、应用程序通过迭代器接口获取对象实例ServiceLoader先判断成员变量providers对象中(LinkedHashMap
//获取迭代器进行加载
public Iterator iterator() {
// 返回迭代器
return new Iterator() {
// 查询缓存中是否存在实例对象
Iterator> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 如果缓存中已经存在返回true
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();
}
};
}
//LazyIterator的类加载
// 判断是否拥有下一个实例
private boolean hasNextService() {
// 如果拥有直接返回true
if (nextName != null) {
return true;
}
// 具体实现类的全名 ,Enumeration config
// 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称
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;
}
// 转换config中的元素,或者具体实现类的真实包结构
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());
// 将实现类加入缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
在实际项目中,主应用,spi接口和spi实现类可能是不同的应用,比如需求:针对不同的银行数据源需要对接,需要不同的具体实现类,在A系统中需要使用提供数据接口供其他系统使用,需要引入不同的银行实现,可以利用SPI实现,主要包括以下系统:
A系统:主应用的SpringBoot 项目,需要引入提供b-spi接口的依赖
b-spi:spi接口的Maven项目,提供一个DataSourceService接口( 或者是抽象类)
c-data:spi实现类的Maven项目,同样需要引入提供b-spi接口的依赖,根据不同银行的需求,实现DataSourceService接口,提供不同的实现类,并在source/META-INF/services目录下提供配置DataSourceService的全限定名的配置文件,文件的内容就是不同实现类的全限类名
最后,为了能够是的A系统通过ServiceLoader加载到c_data项目中的实现类,需要保证c_data的jar包在主程序的classpath中这样可以通过ServiceLoader.load(DataSourceService.class)直接加载,当然实际使用过程中,为了解耦,需要根据c_data打包后的路径直接去加载jar包,这里使用URLClassLoader去加载,大致的代码如下:
//根据路径加载
@PostConstruct
public void init() {
log.info("ExtDataSourceManager init start path:{},dstemplateName:{}", externalDataSourceTemplateConfig.getPath());
ExternalClassLoader externalClassLoader = null;
List jarFileList = externalDataSourceTemplateConfig.getJarFiles();
if(jarFileList.isEmpty()){
log.warn("ExtDataSourceManager loadConfig not find ext datasource jar file.");
return;
}
for(String filePath:jarFileList){
log.info("ExtDataSourceManager loadConfig error filePath:{}",filePath);
try {
externalClassLoader = new ExternalClassLoader(filePath);
} catch (MalformedURLException e) {
}
ServiceLoader services = ServiceLoader
.load(DataSourceService.class, externalClassLoader);
for (DataSourceServiceservice : services) {
String templateName = service.getTemplate().getName();
//放入Map中,后面直接获取
templateServiceByName.put(templateName, service);
}
}
}
class ExternalClassLoader extends URLClassLoader {
public ExternalClassLoader(final String path) throws MalformedURLException {
super(new URL[] { new URL(path)},Thread.currentThread().getContextClassLoader());
}
}
//获取jar的路径
private static final String JAR_FILE_PATH_HEAD = "jar:file:///";
@Value("${spi.datasource-dir}")
private String path; //多个spi实现类的jar所在的路径
private List jarFiles = Lists.newArrayList();
/**
* 获取spi实现类的路径
*/
@PostConstruct
public void init() {
File directory = new File(path);
if (directory != null && directory.isDirectory()) {
Collection tempFiles = FileUtils.listFiles(directory, new String[]{"jar"}, false);
if (tempFiles == null) {
log.error("ExtDsConfig getJarFiles datasource jar dir config error path:{}", path);
}
for (File f : tempFiles) {
if (f.isFile()) {
String fileName = f.getName();
jarFiles.add(buildJarFilePath(fileName));
}
}
}
log.info("ExtDsConfig getJarFiles find datasource file jar size:{}", jarFiles.size());
}
优点:
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点: