Java SPI 源码解析及 demo 讲解

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Java SPI 源码解析及 demo 讲解

  点击上方 **好好学java **,选择 **星标 **公众号


重磅资讯、干货,第一时间送达
今日推荐:Java实现QQ登录和微博登录个人原创+1博客:点击前往,查看更多

作者:JlDang
来源:https://segmentfault.com/a/1190000022101812

1. SPI是什么,有什么用处

SPI全称S而vice Provider Interface,是java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

系统设计的各个抽象,一般有很多不同的实现方案,比如通过不同类型的配置文件加载配置信息,通过不同的序列化方案实现序列化。一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可插拔的原则。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

SPI的核心思想就是解耦

2.使用场景

调用者根据实际需要替换框架的实现策略。

比如常见的例子:

  • 数据库驱动加载类接口实现类的加载,JDBC加载不同类型数据库的驱动- 日志实现类加载- dubbo中也大量使用SPI的方式实现框架的扩展,不过它对java提供的SPI进行了封装

    3.如何使用

实例代码

  1. 定义一组接口,并写出多个实现类
    ```
    package com.djl.test.spi.api;

/**

  • @ClassName Robot
  • @Description TODO
  • @Author djl
  • @Date 2020-03-18 19:01
  • @Version 1.0

*/
public interface Robot {


void sayHello();

}



package com.djl.test.spi.java;

import com.djl.test.spi.api.Robot;

/**

  • @ClassName Bumblebee
  • @Description TODO
  • @Author djl
  • @Date 2020-03-18 19:03
  • @Version 1.0

*/
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println(“Hello ,I am Bumble bee”);
}
}



package com.djl.test.spi.java;

import com.djl.test.spi.api.Robot;

/**

  • @ClassName OptimusPrime
  • @Description TODO
  • @Author djl
  • @Date 2020-03-18 19:02
  • @Version 1.0

*/
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println(“Hello,I am Optimus prime”);
}
}


1. 在META-INF/services文件下,创建一个以接口全限定名命名的文件,内容为实现类的全限定名

com.djl.test.spi.java.Bumblebee
com.djl.test.spi.java.OptimusPrime


1. 通过ServiceLoader类进行加载

import com.djl.test.spi.api.Robot;
import com.djl.test.spi.api.RobotDubbo;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.junit.Test;

import java.util.ServiceLoader;

/**

  • @ClassName JavaSPITest
  • @Description TODO
  • @Author djl
  • @Date 2020-03-18 19:06
  • @Version 1.0

*/
public class JavaSPITest {


@Test
public void sayHello(){
    ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
    System.out.println("java spi");
    serviceLoader.forEach(Robot::sayHello);
}

}


1. 执行结果
## 4 源码解析

我会将分析都写在代码注释中,大家可以打开自己的源码耐心的看一会。

接下来是重头戏了,知道了spi怎么用,那么内部是如何实现的呢?

我们直接从ServiceLoader类的load方法看起。

/**为给定的服务类型创建一个新的服务加载程序,使用
*当前线程的{@linkplain java.lang.Thread#getContextClassLoader
*上下文类装入器}。
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
//1.获取当前线程的类加载
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}</code><code class=”java”>public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) {
//new一个serviceloader对象
return new ServiceLoader<>(service, loader);
}</code><code class=”java”>//构造函数
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//判断入参是否为null
service = Objects.requireNonNull(svc, “Service interface cannot be null”);
//2.加载器如果不存在,获取系统类加载器,通常是applicationLoader,应用程序加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//3.获取访问控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}</code><code class=”java”>public void reload() {
// 清空缓存
providers.clear();
// 初始化内部类,用于遍历提供者
lookupIterator = new LazyIterator(service, loader);
}



看到这里,相信大家对于初始化的内容有了一定了解,这里面涉及到了一些属性,我们来总结下

private static final String PREFIX = “META-INF/services/“;

// 要加载的类
private final Class<S> service;

// 用于加载实现类的类加载器
private final ClassLoader loader;

// 访问控制器
private final AccessControlContext acc;

// 提供者的缓存
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 一个内部类,用于遍历实现类
private LazyIterator lookupIterator;



现在我们发现重点就在于LazyIterator这个内部类上,我们获取实现类都看这个内部类了,我们继续来分析

private class LazyIterator
implements Iterator<S> {


Class&lt;S&gt; service;
ClassLoader loader;
Enumeration&lt;URL&gt; configs = null;
Iterator&lt;String&gt; pending = null;
String nextName = null;

//构造函数
private LazyIterator(Class&lt;S&gt; service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

 private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                //获取META-INF/services下文件全称
                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());
        }
        nextName = pending.next();
        return true;
    }&lt;/code&gt;&lt;code class="java"&gt;private Iterator&lt;String&gt; parse(Class&lt;?&gt; service, URL u)
throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
//存储配置文件中实现类的全限定名
ArrayList&lt;String&gt; names = new ArrayList&lt;&gt;();
try {
    in = u.openStream();
    r = new BufferedReader(new InputStreamReader(in, "utf-8"));
    int lc = 1;
    //读取文件内容,这里不多说了,正常的流操作
    while ((lc = parseLine(service, u, r, lc, names)) &gt;= 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();

}</code><code class=”java”>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 {
//这一行将实例化的类强转成所表示的类型
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
}



//这里是iterable循环遍历
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}

```

到这里整个链路就分析完成了。

感兴趣的小伙伴可以按照demo,自己跑一遍,有问题欢迎提问。

原文地址:https://sihai.blog.csdn.net/article/details/109465303

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Java SPI 源码解析及 demo 讲解


 上一篇
JDK 13 的最新垃圾回收器ZGC,你了解多少? JDK 13 的最新垃圾回收器ZGC,你了解多少?
  点击上方 **好好学java **,选择 **星标 **公众号 重磅资讯、干货,第一时间送达 今日推荐:Java实现QQ登录和微博登录个人原创+1博客:点击前往,查看更多 作者:马小邱 来源:https://segmentfaul
2021-04-04
下一篇 
十分钟了解 git 那些 “不常用” 命令 十分钟了解 git 那些 “不常用” 命令
  点击上方 **好好学java **,选择 **星标 **公众号 重磅资讯、干货,第一时间送达 今日推荐:Java实现QQ登录和微博登录个人原创+1博客:点击前往,查看更多 链接:https://segmentfault.com/a
2021-04-04