Java SPI机制介绍

“本文根据其他文章和文档理解整理,非原创,原作者表示感谢”

SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,它弥补了类加载双亲委派模型的局限、做了很好的补充。广义上来说也可以认为是一种软件设计模式,使得接口与实现解耦,实现面向接口编程。一般用于框架扩展和替换组件实现。

双亲委派类加载模型的局限性

三种类加载器:

  • BootstrapClassLoader 加载rt.jar中的类,所有加载器的根,由底层C++实现
  • ExtClassLoader 扩展类加载器,加载jre/lib/ect目录或java.ext.dirs属性指定的目录下的类
  • AppClassLoader 系统类加载器,加载classpath下的类

以HelloWorld这种代码为例:.java文件编译为.class文件,然后jvm加载.class文件到内存,就是执行所谓类加载,到方法区生成类的描述、方法、静态成员等,并在堆区生成对应的Class对象。
比如我们在HelloWorld里用到了HashMap,AppClassLoader加载我们写的HelloWorld应用程序,然后如果使用java核心类库的比如HashMap这些,那么就会双亲委派,最终会由BootstrapClassLoader来加载HashMap类。

父ClassLoader无法使用子ClassLoader加载的类
考虑一种场景:比如类似jdbc,核心包里边有interface api,然后实现impl.jar肯定是开发者通过classpath引入到应用程序里的,这由AppClassLoader加载了。
应用程序使用jdbc的功能,然后双亲委派,最终BootstrapClassLoader会发现加载过api,但是会发现自己这个类加载器(加载的类里)没有impl.jar实现,所以无法工作。
即,如下加载关系:
BootstrapClassLoader -> api interface
AppClassLoader -> impl

这该咋办?

  1. 线程上下文类加载器,
    默认情况下就是AppClassLoader,可以通过Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。
  2. SPI
    使用ServiceLoader ,会根据约定从classpath下的META-INF/services/目录找文件名为接口全限定类目的配置文件,比如文件名为com.wangan.interfaceName,然后这个配置里边会配置这个接口的实现类的名字,这样就可以加载到实现类然后使用了。

接下来通过一个简单的例子体会一下JDK的SPI,以及所谓面向接口编程和模块化。

一个实现类implments接口的例子
//接口
package com.wangan.spi;
public interface SPIService {
    public void execute();
}
//实现
public class SPIServiceImplA implements SPIService{

    @Override
    public void execute() {
        System.out.println("SPIServiceImplA execute");
        
    }

}

我们假设实现类是通过独立的implA.jar包,引入到当前工程里的,然后通常我们可以按照如下方式用:

SPIService service = new SPIServiceImplA();
service.execute();

这当然可行,但是这个做有个不完美的地方:

如果有一天接口SPIService需要替换为一个SPIServiceImplB的实现,那么需要:

1、替换引入的implA.jar为implB.jar;

2、修改所有使用SPIService的代码。简单来说就是所谓的耦合。

软件设计教科书告诉我们要面向接口进行编程,当前的业务模块也就是我们这个工程,跟依赖的模块也就是implA.jar应该减少耦合,应该依赖双发共同约定的interface、也就是SPIService进行配合编程。业务模块里边理论上不应该出现implements实现的代码,而上面service = new SPIServiceImplA()就破坏了这个规则。

PS:这是单体服务里的解耦合,可以类比一下微服务之间的RPC调用(比如Dubbo、Feigh等),客户端只有接口的定义部分,而实现部分仅出现在服务端。

OK,按照上面的思路,我们看看使用Java的SPI应该怎么做。

使用Java SPI解耦合,实现模块之间面向接口编程

src/META-INF/services目录下新建com.wangan.spi.SPIService文件,内容:

com.wangan.spi.SPIServiceImplA
com.wangan.spi.SPIServiceImplB

然后加载使用:

import java.util.Iterator;
import java.util.ServiceLoader;

public class SPITest {
    
    public static void main(String[] args) {
        ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);
        Iterator<SPIService> ite = loader.iterator();
        while(ite.hasNext()) {
            service = ite.next();
            service.execute();
        }
    }
}

输出:

SPIServiceImplA execute
SPIServiceImplB execute

优势一下就体现出来了,比如上面说的那个情况,换实现的时候只需要把implA.jar从业务工程里去掉,然后引入新的实现包,在implB.jar包里配置好com.wangan.spi.SPIService文件指明实现类是SPIServiceImplB就行了,业务模块调用的地方一行代码也不用改!

源码走读

首先,java.util.ServiceLoader是在rt.jar里的,所以它是BootstrapClassLoader加载的。

ServiceLoader类一开始是个成员变量PREFIX,就是配置文件的位置,其他关键代码如下:

private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

//load方法
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl); //new ServiceLoader<>(service, loader)
}
//返回的是ServiceLoader实例,其实现了Iterator接口
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    //加载的接口不能为null
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //传进来的ClassLoader如果为空就使用SystemClassLoader
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    //访问控制
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

可以看到lookupIterator是一个LazyIterator实例,这是一个内部类、也是实现了Iterator接口。

然后我们回到我们的代码,通过loader = ServiceLoader.load(SPIService.class)拿到ServiceLoader之后,loader.iterator()取迭代器,然后通过ite.hasNext()ite.next()去拿到我们的实现类的实例的,所以接下重点看看这几个方法的实现就好了。so,接着看:

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

我们可以看到,最终都是用的lookupIterator也就是LazyIterator的对应方法,所以真正加载逻辑都是在LazyIterator这个内部类里。关键代码如下:

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());
    }
    nextName = pending.next();
    return true;
}
//hasNextService是使用class loader加载resource配置文件,然后遍历里边的配置项目。

//next()方法最终调的是nextService():
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);   //这里的loader可以确保能够使用我们的实现类,据此得到实现类的Class对象
    } 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());    //得到实现类的实例,并Class.cast(Object obj)转成接口类型
        providers.put(cn, p);
        return p;   //返回接口实现类的实例
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

至此,搞清楚了,用的是Java反射的方法得到的实现类的实例。
值得一提的是反射得到实例之前,确保了ClassLoader是实现类对应的加载器,回忆一下之前的ServiceLoader.load()方法里用的class loader是Thread.currentThread().getContextClassLoader()ClassLoader.getSystemClassLoader()。这是个很关键的点。
还记得前文提到过的吗,父ClassLoader无法使用子ClassLoader加载的类,所以要先知道是哪个子ClassLoader加载的这个类,然后用反射的方式实例化这个类的实例。

实际案例有JDBC驱动,Apache common-logging等等,特别适合这种技术标准组织指定某种规范,然后各个提供商去做自己的实现的场景。

Spring里的SPI

好累,半天就写了这点东西。果然人类是有极限的吗?喔勒哇 您gen 喔 呀me撸zo JoJo !!!


学的越多、不会的越多,往脑袋里强灌知识的感觉很累,但搞懂之后的求知所得的感觉又非常的快乐。

参考

Java技术专题-带你深入理解和认识SPI运作机制 - 简书 (jianshu.com)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容