写点什么

SPI:Java 的高可扩展利器

  • 2022 年 8 月 16 日
    中国香港
  • 本文字数:11462 字

    阅读完需:约 38 分钟

SPI:Java的高可扩展利器

本文分享自华为云社区《一文讲透Java核心技术之高可扩展利器SPI》,作者: 冰 河。

SPI 的概念


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.MyServiceAio.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 Worldio.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 机制


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
SPI:Java的高可扩展利器_Java_华为云开发者联盟_InfoQ写作社区