关于后端:对Java中SPI的理解

41次阅读

共计 4463 个字符,预计需要花费 12 分钟才能阅读完成。

什么是 SPI

SPI 全称 Service Provider Interface,字面意思是提供服务的接口,再解释具体一下就是Java 提供的一套用来被第三方实现或扩大的接口,实现了接口的动静扩大,让第三方的实现类能像插件一样嵌入到零碎中。

咦。。。
这个解释感觉还是有点绕口。
那就说一下它的实质。

将接口的实现类的全限定名配置在文件中(文件名是接口的全限定名),由服务加载器读取配置文件,加载实现类。实现了运行时动静为接口替换实现类。

SPI 示例

还是举例说明吧。
咱们创立一个我的项目,而后创立一个 module 叫 spi-interface。

在这个 module 中咱们定义一个接口:

/**
 * @author jimoer
 **/
public interface SpiInterfaceService {

    /**
     * 打印参数
     * @param parameter 参数
     */
    void printParameter(String parameter);
}

再定义一个 module,名字叫 spi-service-one,pom.xml 中依赖 spi-interface。
在 spi-service-one 中定义一个实现类,实现 SpiInterfaceService 接口。

package com.jimoer.spi.service.one;
import com.jimoer.spi.app.SpiInterfaceService;

/**
 * @author jimoer
 **/
public class SpiOneService implements SpiInterfaceService {
    /**
     * 打印参数
     *
     * @param parameter 参数
     */
    @Override
    public void printParameter(String parameter) {System.out.println("我是 SpiOneService:"+parameter);
    }
}

而后在 spi-service-one 的 resources 目录下创立目录 META-INF/services,在此目录下创立一个文件名称为 SpiInterfaceService 接口的全限定名称,文件内容写入 SpiOneService 这个实现类的全限定名称。
成果如下:

再创立一个 module,名称为:spi-service-one,也是依赖 spi-interface,并且定义一个实现类 SpiTwoService 来实现 SpiInterfaceService 接口。

package com.jimoer.spi.service.two;
import com.jimoer.spi.app.SpiInterfaceService;
/**
 * @author jimoer
 **/
public class SpiTwoService implements SpiInterfaceService {
    /**
     * 打印参数
     *
     * @param parameter 参数
     */
    @Override
    public void printParameter(String parameter) {System.out.println("我是 SpiTwoService:"+parameter);
    }
}

目录构造如下:

上面再创立一个用来测试的 module,名为:spi-app。

pom.xml 中依赖 spi-service-one 和 spi-service-two

<dependencies>
    <dependency>
        <groupId>com.jimoer.spi</groupId>
        <artifactId>spi-service-one</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.jimoer.spi</groupId>
        <artifactId>spi-service-two</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies> 

创立测试类

/**
 * @author jimoer
 **/
public class SpiService {public static void main(String[] args) {ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);
        Iterator<SpiInterfaceService> iterator = spiInterfaceServices.iterator();
        while (iterator.hasNext()){SpiInterfaceService sip = iterator.next();
            sip.printParameter("参数");
        }
    }
}

执行后果:

我是 SpiTwoService: 参数
我是 SpiOneService: 参数

通过运行后果咱们能够看到,曾经将 SpiInterfaceService 接口的所有实现都加载到了以后我的项目中,并且执行了调用。

这整个代码构造咱们能够看出 SPI 机制将模块的拆卸放到了程序里面,就是说,接口的实现能够在程序里面,只须要在应用的时候指定具体的实现。并且动静的加载到本人的我的项目中。
SPI 机制的次要目标:
一是为理解耦,将接口和具体实现拆散开来;
二是进步框架的扩展性。以前写程序的时候,接口和实现都写在一起,调用方在应用的时候依赖接口来进行调用,无权抉择应用具体的实现类。

SPI 的实现

那么咱们来看一下 SPI 具体是如何实现的呢?
通过下面的例子,咱们能够看到,SPI 机制的外围代码是上面这段:

ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);

那么咱们来看一下 ServiceLoader.load()办法的源码:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

看到 Thread.currentThread().getContextClassLoader();我就明确是怎么回事了,这个就是 线程上下文类加载器 ,因为 线程上下文类加载器 就是为了做类加载双亲委派模型的逆序而创立的。

应用这个线程上下文类加载器去加载所需的 SPI 服务代码,这是一种父类加载器去申请子类加载器实现类加载的行为,这种行为实际上是买通了,双亲委派模型的层次结构来逆向应用类加载器,曾经违反了双亲委派模型的一般性准则,但也是无可奈何的事件。
《深刻了解 Java 虚拟机(第三版)》

尽管晓得了它是毁坏双亲委派的了,然而具体实现,还是须要具体往下看的。

在 ServiceLoader 里找到具体实现 hasNext()的办法了,那么持续来看这个办法的实现。

hasNext()办法又次要调用了 hasNextService()办法。

// 固定门路
private static final String PREFIX = "META-INF/services/";

private boolean hasNextService() {if (nextName != null) {return true;}
     if (configs == null) {
         try {
             // 固定门路 + 接口全限定名称
             String fullName = PREFIX + service.getName();
             // 如果以后线程上下文类加载器为空,会用父类加载器(默认是应用程序类加载器)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()) {if (!configs.hasMoreElements()) {return false;}
         pending = parse(service, configs.nextElement());
     }
     // 前面 next()办法中判断以后类是否曾经呈现化的时候要用
     nextName = pending.next();
     return true;
 }

次要就是去加载 META-INF/services/ 门路下的接口全限定名称的文件而后去外面找到实现类的类门路将实现类进行类加载。

持续看迭代器是如何取出每一个实现对象的。那就要看 ServiceLoader 中实现了迭代器的 next()办法了。

next()办法次要是 nextService()实现的,那么持续看 nextService()办法。

private S nextService() {if (!hasNextService())
         throw new NoSuchElementException();
     String cn = nextName;
     nextName = null;
     Class<?> c = null;
     try {// 间接加载类,无需初始化(因为下面 hasNext()曾经初始化了)。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}

看到这里就能够明确了,是如何创立出对象的了。先在 hasNext()将接口的实现类进行加载并判断是否存在接口的实现类,而后在 next()办法中将实现类进实例化。

Java 中应用 SPI 机制的性能其实有很多,像 JDBC、JNDI、以及 Spring 中也有应用,甚至 RPC 框架(Dubbo)中也有应用 SPI 机制来实现性能。

正文完
 0