写点什么

结合实战和源码来聊聊 Java 中的 SPI 机制?

用户头像
冰河
关注
发布于: 2020 年 11 月 20 日
结合实战和源码来聊聊Java中的SPI机制?

写在前面



SPI机制能够非常方便的为某个接口动态指定其实现类,在某种程度上,这也是某些框架具有高度可扩展性的基础。今天,我们就从源码级别深入探讨下Java中的SPI机制。



注:文章已收录到:https://github.com/sunshinelyz/technology-binghe



SPI的概念



SPI在Java中的全称为Service Provider Interface,是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。



JAVA SPI = 基于接口的编程+策略模式+配置文件的动态加载机制



SPI的使用场景



Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。



大量的框架使用了Java的SPI技术,如下:



(1)JDBC加载不同类型的数据库驱动

(2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类

(3)Spring中大量使用了SPI



  • 对servlet3.0规范

  • 对ServletContainerInitializer的实现

  • 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等



(4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现



SPI的使用



当服务的提供者,提供了接口的一种实现后,需要在Jar包的META-INF/services/目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名)。



当外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该Jar包的META/Services/目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。



SPI中查找接口的实现类是通过java.util.ServiceLoader,而在java.util.ServiceLoader类中有一行代码如下:



// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";



这也就是说,我们必须将接口的配置文件写到Jar包的META/Services/目录下。



SPI实例



这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。



注意:实例是基于Java8进行开发的。



1.创建Maven项目



在IDEA中创建Maven项目spi-demo,如下:





2.编辑pom.xml



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>spi-demo</artifactId>
<groupId>io.binghe.spi</groupId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>



3.创建类加载工具类



在io.binghe.spi.loader包下创建MyServiceLoader,MyServiceLoader类中直接调用JDK的ServiceLoader类加载Class。代码如下所示。



package io.binghe.spi.loader;
import java.util.ServiceLoader;
/**
* @author binghe
* @version 1.0.0
* @description 类加载工具
*/
public class MyServiceLoader {
/**
* 使用SPI机制加载所有的Class
*/
public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) {
return ServiceLoader.load(clazz);
}
}



4.创建接口



在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示:



package io.binghe.spi.service;
/**
* @author binghe
* @version 1.0.0
* @description 定义接口
*/
public interface MyService {
/**
* 打印信息
*/
void print(String info);
}



5.创建接口的实现类



(1)创建第一个实现类MyServiceA



在io.binghe.spi.service.impl包下创建MyServiceA类,实现MyService接口。代码如下所示:



package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
/**
* @author binghe
* @version 1.0.0
* @description 接口的第一个实现
*/
public class MyServiceA implements MyService {
@Override
public void print(String info) {
System.out.println(MyServiceA.class.getName() + " print " + info);
}
}



(2)创建第二个实现类MyServiceB



在io.binghe.spi.service.impl包下创建MyServiceB类,实现MyService接口。代码如下所示:



package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
/**
* @author binghe
* @version 1.0.0
* @description 接口第二个实现
*/
public class MyServiceB implements MyService {
@Override
public void print(String info) {
System.out.println(MyServiceB.class.getName() + " print " + info);
}
}



6.创建接口文件



在项目的src/main/resources目录下创建META/Services/目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示:



io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB



7.创建测试类



在项目的io.binghe.spi.main包下创建Main类,该类为测试程序的入口类,提供一个main()方法,在main()方法中调用ServiceLoader类加载MyService接口的实现类。并通过Java8的Stream将结果打印出来,如下所示:



package io.binghe.spi.main;
import io.binghe.spi.loader.MyServiceLoader;
import io.binghe.spi.service.MyService;
import java.util.ServiceLoader;
import java.util.stream.StreamSupport;
/**
* @author binghe
* @version 1.0.0
* @description 测试的main方法
*/
public class Main {
public static void main(String[] args){
ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class);
StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
}
}



8.测试实例



运行Main类中的main()方法,打印出的信息如下所示:



io.binghe.spi.service.impl.MyServiceA print Hello World
io.binghe.spi.service.impl.MyServiceB print Hello World
Process finished with exit code 0



通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法。



源码解析



这里,主要是对SPI的加载流程涉及到的java.util.ServiceLoader的源码的解析。



进入java.util.ServiceLoader的源码,可以看到ServiceLoader类实现了java.lang.Iterable接口,如下所示。



public final class ServiceLoader<S> implements Iterable<S>



说明ServiceLoader类是可以遍历迭代的。



java.util.ServiceLoader类中定义了如下的成员变量:



// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";
// 需要加载的接口
private final Class<S> service;
// 类加载器,用于加载以接口命名的文件中配置的接口的实现类
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文环境
private final AccessControlContext acc;
// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用于延迟加载实现类的迭代器
private LazyIterator lookupIterator;



可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的src/main/resources目录下的META-INF/services/目录下创建。



从MyServiceLoader类调用ServiceLoader.load(clazz)方法进入源码,如下所示:



//根据类的Class对象加载指定的类,返回ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//动态加载指定的类,将类加载到ServiceLoader中
return ServiceLoader.load(service, cl);
}



方法中调用了ServiceLoader.load(service, cl)方法,继续跟踪代码,如下所示:



//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}



可以看到ServiceLoader.load(service, cl)方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示:



//构造ServiceLoader对象
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果传入的Class对象为空,则判处空指针异常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}



继续跟reload()方法,如下所示。



//重新加载
public void reload() {
//清空保存加载的实现类的LinkedHashMap
providers.clear();
//构造延迟加载的迭代器
lookupIterator = new LazyIterator(service, loader);
}



继续跟进懒加载迭代器的构造函数,如下所示。



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



可以看到,会将需要加载的接口的Class对象和类加载器赋值给LazyIterator的成员变量。



当我们在程序中迭代获取对象实例时,首先在成员变量providers中查找是否有缓存的实例对象。如果存在则直接返回,否则调用lookupIterator延迟加载迭代器进行加载。



迭代器进行逻辑判断的代码如下所示:



//迭代ServiceLoader的方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//获取保存实现类的LinkedHashMap<String,S>的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
//判断是否有下一个元素
public boolean hasNext() {
//如果knownProviders存在元素,则直接返回true
if (knownProviders.hasNext())
return true;
//返回延迟加载器是否存在元素
return lookupIterator.hasNext();
}
//获取下一个元素
public S next() {
//如果knownProviders存在元素,则直接获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//获取延迟迭代器lookupIterator中的元素
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}



LazyIterator加载类的流程如下代码所示



//判断是否拥有下一个实例
private boolean hasNextService() {
//如果拥有下一个实例,直接返回true
if (nextName != null) {
return true;
}
//如果实现类的全名为null
if (configs == null) {
try {
//获取全文件名,文件相对路径+文件名称(包名+接口名)
String fullName = PREFIX + service.getName();
//类加载器为空,则通过ClassLoader.getSystemResources()方法获取
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()) {
//如果configs中没有更过的元素,则直接返回false
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 {
//通过c.newInstance()生成对象实例
S p = service.cast(c.newInstance());
//将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
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);
}
}



最后,给出整个java.util.ServiceLoader的类,如下所示:



package java.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
public final class ServiceLoader<S> implements Iterable<S> {
// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";
// 需要加载的接口
private final Class<S> service;
// 类加载器,用于加载以接口命名的文件中配置的接口的实现类
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文环境
private final AccessControlContext acc;
// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用于延迟加载实现类的迭代器
private LazyIterator lookupIterator;
//重新加载
public void reload() {
//清空保存加载的实现类的LinkedHashMap
providers.clear();
//构造延迟加载的迭代器
lookupIterator = new LazyIterator(service, loader);
}
//构造ServiceLoader对象
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果传入的Class对象为空,则判处空指针异常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
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;
}
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();
}
// Private inner class implementing fully-lazy provider lookupload
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() {
//如果拥有下一个实例,直接返回true
if (nextName != null) {
return true;
}
//如果实现类的全名为null
if (configs == null) {
try {
//获取全文件名,文件相对路径+文件名称(包名+接口名)
String fullName = PREFIX + service.getName();
//类加载器为空,则通过ClassLoader.getSystemResources()方法获取
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()) {
//如果configs中没有更过的元素,则直接返回false
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 {
//通过c.newInstance()生成对象实例
S p = service.cast(c.newInstance());
//将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
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();
}
}
//迭代ServiceLoader的方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//获取保存实现类的LinkedHashMap<String,S>的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
//判断是否有下一个元素
public boolean hasNext() {
//如果knownProviders存在元素,则直接返回true
if (knownProviders.hasNext())
return true;
//返回延迟加载器是否存在元素
return lookupIterator.hasNext();
}
//获取下一个元素
public S next() {
//如果knownProviders存在元素,则直接获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//获取延迟迭代器lookupIterator中的元素
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
//根据类的Class对象加载指定的类,返回ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//动态加载指定的类,将类加载到ServiceLoader中
return ServiceLoader.load(service, cl);
}
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);
}
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}



SPI总结



最后,对Java提供的SPI机制进行简单的总结。



优点:



能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。



缺点:



  • 多个并发多线程使用ServiceLoader类的实例是不安全的

  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。



参考:深入理解Java中的spi机制



重磅福利



微信搜一搜【冰河技术】微信公众号,关注这个有深度的程序员,每天阅读超硬核技术干货,公众号内回复【PDF】有我准备的一线大厂面试资料和我原创的超硬核PDF技术文档,以及我为大家精心准备的多套简历模板(不断更新中),希望大家都能找到心仪的工作,学习是一条时而郁郁寡欢,时而开怀大笑的路,加油。如果你通过努力成功进入到了心仪的公司,一定不要懈怠放松,职场成长和新技术学习一样,不进则退。如果有幸我们江湖再见!



另外,我开源的各个PDF,后续我都会持续更新和维护,感谢大家长期以来对冰河的支持!!



写在最后



如果你觉得冰河写的还不错,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发、分布式、微服务、大数据、互联网和云原生技术,「 冰河技术 」微信公众号更新了大量技术专题,每一篇技术文章干货满满!不少读者已经通过阅读「 冰河技术 」微信公众号文章,吊打面试官,成功跳槽到大厂;也有不少读者实现了技术上的飞跃,成为公司的技术骨干!如果你也想像他们一样提升自己的能力,实现技术能力的飞跃,进大厂,升职加薪,那就关注「 冰河技术 」微信公众号吧,每天更新超硬核技术干货,让你对如何提升技术能力不再迷茫!





发布于: 2020 年 11 月 20 日阅读数: 31
用户头像

冰河

关注

公众号:冰河技术 2020.05.29 加入

Mykit系列开源框架发起者、核心架构师和开发者,《海量数据处理与大数据技术实战》与《MySQL开发、优化与运维实战》作者。【冰河技术】微信公众号作者。

评论

发布
暂无评论
结合实战和源码来聊聊Java中的SPI机制?