JAVA中可扩展工具SPI详析

  SPI的概念
  
  SPI=基于接口的编程+策略模式+配置文件的动态加载机制
  
  SPI的使用场景
  
  Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的 SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。
  
  大量的框架使用了Java的SPI技术,如下:
  
  1、JDBC加载不同类型的数据库驱动
  
  2、日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
  
  3、Spring中大量使用了SPI
  
  4、Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现
  
  SPI 的使用
  
  当服务的提供者,提供了接口的一种实现后,需要在 Jar 包的 META-INF/services/目录下,创建一个以接口的名称(包名。接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名 + 类名)。
  
  当外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该 ar包的 META/Services/目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI 的规范规定了接口的实现类必须有一个无参构造方法。
  
  SPI中查找接口的实现类是通过java.util.ServiceLoader,而在 java.util.ServiceLoader类中有一行代码如下:

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

  SPI实例
  
  这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。
  
  注意:实例是基于Java8进行开发的。
  
  1.创建Maven项目
  
  在IDEA中创建Maven项目spi-demo,如下:

JAVA中可扩展工具SPI详析

  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.创建接口的实现类
  
  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);
 }
}

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

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

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

最后,给出整个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总结
  
  优点:
  
  能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
  
  缺点:
  
  ●多个并发多线程使用ServiceLoader类的实例是不安全的
  
  ●虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

版权声明:程序员胖胖胖虎阿 发表于 2022年8月31日 上午2:48。
转载请注明:JAVA中可扩展工具SPI详析 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...