- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
**摘要:**JAVA SPI,基于接口的编程+策略模式+配置文件的动态加载机制。
本文分享自华为云社区《一文讲透Java核心技术之高可扩展利器SPI》,作者: 冰 河。
JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制
Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。
大量的框架使用了Java的SPI技术,如下:
(1)JDBC加载不同类型的数据库驱动
(2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
(3)Spring中大量使用了SPI
(4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现
当服务的提供者,提供了接口的一种实现后,需要在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使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。
注意:实例是基于Java8进行开发的。
在IDEA中创建Maven项目spi-demo,如下:
spi-demo io.binghe.spi jar 1.0.0-SNAPSHOT 4.0.0 org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin 3.6.0 1.8 1.8
在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 ServiceLoader loadAll(final Class clazz) { return ServiceLoader.load(clazz); } }
在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示:
package io.binghe.spi.service; /** * @author binghe * @version 1.0.0 * @description 定义接口 */ public interface MyService { /** * 打印信息 */ void print(String info); }
在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); } }
在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); } }
在项目的src/main/resources目录下创建**META/Services/**目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示:
io.binghe.spi.service.impl.MyServiceA io.binghe.spi.service.impl.MyServiceB
在项目的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 loader = MyServiceLoader.loadAll(MyService.class); StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World")); } }
运行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的源码的解析。
Đi vàojava.util.ServiceLoader的源码,可以看到ServiceLoader类实现了java.lang.Iterable接口,如下所示。
public final class ServiceLoader implements Iterable
说明ServiceLoader类是可以遍历迭代的。
java.util.ServiceLoader类中定义了如下的成员变量:
// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下 private static final String PREFIX = "META-INF/services/"; // 需要加载的接口 private final Class service; // 类加载器,用于加载以接口命名的文件中配置的接口的实现类 private final ClassLoader loader; // 创建ServiceLoader时采用的访问控制上下文环境 private final AccessControlContext acc; // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象 private LinkedHashMap providers = new LinkedHashMap<>(); // 用于延迟加载实现类的迭代器 private LazyIterator lookupIterator;
可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的src/main/resources目录下的**META-INF/services/**目录下创建。
从MyServiceLoader类调用**ServiceLoader.load(clazz)**方法进入源码,如下所示:
//根据类的Class对象加载指定的类,返回ServiceLoader对象 public static ServiceLoader load(Class service) { //获取当前线程的类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); //动态加载指定的类,将类加载到ServiceLoader中 return ServiceLoader.load(service, cl); }
方法中调用了**ServiceLoader.load(service, cl)**方法,继续跟踪代码,如下所示:
//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中 public static ServiceLoader load(Class service, ClassLoader loader){ return new ServiceLoader<>(service, loader); }
可以看到**ServiceLoader.load(service, cl)**方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示:
//构造ServiceLoader对象 private ServiceLoader(Class 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 service, ClassLoader loader) { this.service = service; this.loader = loader; }
可以看到,会将需要加载的接口的Class对象和类加载器赋值给LazyIterator的成员变量。
当我们在程序中迭代获取对象实例时,首先在成员变量providers中查找是否有缓存的实例对象。如果存在则直接返回,否则调用lookupIterator延迟加载迭代器进行加载。
迭代器进行逻辑判断的代码如下所示:
//迭代ServiceLoader的方法 public Iterator iterator() { return new Iterator() { //获取保存实现类的LinkedHashMap的迭代器 Iterator<>> 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) 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 action = new PrivilegedAction() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { 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 implements Iterable { // 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下 private static final String PREFIX = "META-INF/services/"; // 需要加载的接口 private final Class service; // 类加载器,用于加载以接口命名的文件中配置的接口的实现类 private final ClassLoader loader; // 创建ServiceLoader时采用的访问控制上下文环境 private final AccessControlContext acc; // 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象 private LinkedHashMap providers = new LinkedHashMap<>(); // 用于延迟加载实现类的迭代器 private LazyIterator lookupIterator; //重新加载 public void reload() { //清空保存加载的实现类的LinkedHashMap providers.clear(); //构造延迟加载的迭代器 lookupIterator = new LazyIterator(service, loader); } //构造ServiceLoader对象 private ServiceLoader(Class 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 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 parse(Class> service, URL u) throws ServiceConfigurationError { InputStream in = null; BufferedReader r = null; ArrayList 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 { Class service; ClassLoader loader; Enumeration configs = null; Iterator pending = null; String nextName = null; private LazyIterator(Class 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) 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 action = new PrivilegedAction() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } } //迭代ServiceLoader的方法 public Iterator iterator() { return new Iterator() { //获取保存实现类的LinkedHashMap的迭代器 Iterator<>> 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 ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } //根据类的Class对象加载指定的类,返回ServiceLoader对象 public static ServiceLoader load(Class service) { //获取当前线程的类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); //动态加载指定的类,将类加载到ServiceLoader中 return ServiceLoader.load(service, cl); } public static ServiceLoader loadInstalled(Class 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() + "]"; } }
能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
参考:深入理解Java中的spi机制
我的内部类如下: public class ClassWithInnerObject { private final InnerObject innerObject; public Class
我在 STM32F4 Discovery 上安装了 STM32F4407VGT6 Controller 。我尝试使用 SPI + DMA 从 AD7683 ADC 读取数据,但 DMA 接收缓冲区始终
假设您有一个只有一个片选的 SPI 总线。 是否有可以将 8 个或更多设备连接到该 SPI 总线的芯片? 为简化起见,您可以假设所有设备都同意 SPI 模式(数据需要在上升沿有效)。此外,所有设备都是
我有一个 32 GB 的金士顿 SDHC microSD 卡,它必须与 MSP430F2618 通信通过 SPI .我无法通过使用 CMD55 + ACMD41(bit30 设置为 1)来初始化它,如
我正在使用 chai-spies 来确保调用 Controller 中的函数,这是我的测试: it('Should show right season and analysts when compet
我刚开始进行 JS 单元测试,但很难理解如何使用 Jasmine spy 创建有意义的测试。 it('should take an array of shopping items', function
我一直在阅读 SPI 以及如何创建内核驱动程序,但我仍然不确定所有这些是如何工作的。 例如: static struct spi_driver ds1305_driver = {
这是我正在测试的代码 eventsApp.factory('userData', ['userResource', function(userResource){ return{ ge
我有一个简单的组件: .html: {{title}} Change title .ts: export class AppComponent { title = 'app works!'
当我从 SPI 总线上的 PIC-18F4520 向我的卡发出地址为 (0x00000000) 的 cmd17 时,我从命令问题中获得了正确的返回 R1 token 。然后,经过几次循环检查后,我从发
我正在使用rspec-spies我想知道在打完所有电话后是否有办法检查 spy 。 例如,如果我做类似的事情 # setup Post.stub(:find).with(@post.id).and_r
我正在使用 rspec-spies并且想知道是否有办法在所有电话都打完后检查 spy 。 例如,如果我做类似的事情 # setup Post.stub(:find).with(@post.id).an
我正在尝试制作一个可以点击另一个程序按钮的程序。我被告知我需要使用 spy++ 来获取我想要点击的按钮的 ID,所以我现在正在使用它。我找到了包含我希望从中获取按钮 ID 的按钮的窗口(窗口中有 3
问题 在我们的代码库中,我们有一个 sinon 问题,可以使用下面的代码片段重现。问题是,它似乎是间接调用的 spy 返回力 false,console.log 明确指出该方法被调用但 spy.cal
考虑以下 react 组件。 import React, { Component } from "react"; import { reduxForm, Field } from "redux-for
我正在开发一个 Linux spi 驱动程序来处理通过 SPI 端口的通信。 我的 SoC 提供了三个 spi 模块(我将其理解为端口),称为 ecspi1/ecspi2/ecspi3。 我需要使用
当我将类的方法包装成这样的 Sinon-spy 时: sinon.spy(myObject, "myMethod") spy 内部会发生什么? 我猜 spy 对象有一个指向“myObject.myMe
我正在为遗留代码编写一些 JUnit 测试,并且我非常喜欢使用注释。我想知道是否可以创建一个 spy 对象的声明,然后实例化它。我问的原因是因为我有一个带有非空构造函数的类。该构造函数的值直到测试用例
我有两个不同的设备要连接 Arduino .一个Ethernet屏蔽和轴编码器。第一个有 SPI模式 0 和第二个 SPI 模式 2。它们冲突。这个问题有解决方案吗? 我使用不同的芯片选择引脚,这两个
我正在运行使用 Yocto (Pyro) 构建的嵌入式 Linux (4.14.16)。我在具有 i.MX6DL 且 SPI 连接到 FPGA(Xilinx Artix 7)的定制板上运行。我目前正在
Tôi là một lập trình viên xuất sắc, rất giỏi!