文章目录
Java SPI
SPI : Service Provider Interface 为某个接口寻找服务实现的机制。
1. 为何需要SPI
基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Usually API and SPI are separate. For example, in JDBC the Driver class is part of the SPI: If you simply want to use JDBC, you don’t need to use it directly, but everyone who implements a JDBC driver must implement that class.
2. 默认约定
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。
3. 使用案例
3.1 common-logging
common-logging apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,来发现日志实现类。只要我们的日志实现里包含了这个文件,并在文件里指定了 LogFactory工厂接口的实现类即可。
3.2 Spring
在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
3.3 JDBC
在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。
-
JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。 -
mysql实现
在mysql的jar包中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。 -
postgresql实现
同样在postgresql的jar包中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。
3.4 Eclipse插件
Eclipse使用OSGi作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。 一般来说,插件的文件结构必须在指定目录下包含以下三个文件: META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等
build.properties: 项目的编译配置信息,包括,源代码路径、输出路径
plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等.
4. 自己用ServiceLoader实现SPI
定义一个接口和相关的实现类
public interface Save {
void save(String message);
}
public class FileSaver implements Save{
@Override
public void save(String message) {
System.out.println("saveit in file");
}
}
public class DBSaver implements Save{
@Override
public void save(String message) {
System.out.println("saveit in DB");
}
}
在Resource目录下建目录 META-INF/services/ 和文件 com.spi.Save文件。注意文件名就是接口的全路径名称。
然后在文件内写明实现类。
com.spi.DBSaver
com.spi.FileSaver
这样子在Main里实现接口实现类的自动查找与实例化。
public class Main {
public static void main(String[] args) {
ServiceLoader<Save> load = ServiceLoader.load(Save.class);
Iterator<Save> iterator = load.iterator();
while (iterator.hasNext()){
Save save = iterator.next();
save.save("Hello");
}
}
}
运行结果:
saveit in DB
saveit in file
5. ServiceLoader源码
ServiceLoader类内的迭代器有一个LazyIterator,其中会根据文本加载类:
c = Class.forName(cn, false, loader);
并且实例化:S p = service.cast(c.newInstance());
。
private class LazyIterator
implements Iterator<S>
{
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
}
}
总结
SPI将装配的控制权移到了程序之外。
只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想。
java基础 系列在github上有一个开源项目,主要是本系列博客的demo代码。https://github.com/forestnlp/javabasic
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。