绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
深入理解 Java 中 SPI 机制
2019-09-11 10:58:41

本文于 vivo互联网技术 微信公众号

链接: https://mp.weixin.qq.com/s/vpy5DJ-hhn0iOyp747oL5A

作者:姜柱

一、简介

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻zhao服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。

SPI与API区别:

API是调用并用于实现目标的类、接口、方法等的描述;

SPI是扩展和实现以实现目标的类、接口、方法等的描述;

换句话说,API 为操作提供特定的类、方法,SPI 通过操作来符合特定的类、方法。

参考: https://stackoverflow.com/questions/2954372/difference-between-spi-and-api?answertab=votes#tab-top

SPI整体机制图如下:

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查zhao服务的实现的工具类是:java.util.ServiceLoader。

二、应用场景

SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo等等。

SPI流程:

有关组织和公式定义接口标准

第三方提供具体实现: 实现具体方法, 配置 META-INF/services/${interface_name} 文件

开发者使用

比如JDBC场景下:

首先在Java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商提供。

在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。

同样在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是PostgreSQL对Java的java.sql.Driver的实现。

三、使用demo

1.定义一个接口HelloSPI。

1

2

3

packagecom.vivo.study.spidemo.spi;publicinterfaceHelloSPI {

    voidsayHello();

}

2.完成接口的多个实现。

1

2

3

4

5

packagecom.vivo.study.spidemo.spi.impl;importcom.vivo.study.spidemo.spi.HelloSPI;publicclassImageHello implementsHelloSPI {

    publicvoidsayHello() {

        System.out.println("Image Hello");

    }

}

1

2

3

4

5

packagecom.vivo.study.spidemo.spi.impl;importcom.vivo.study.spidemo.spi.HelloSPI;publicclassTextHello implementsHelloSPI {

    publicvoidsayHello() {

        System.out.println("Text Hello");

    }

}

在META-INF/services/目录里创建一个以com.vivo.study.spidemo.spi.HelloSPI的文件,这个文件里的内容就是这个接口的具体的实现类。

具体内容如下:

1

2

com.vivo.study.spidemo.spi.impl.ImageHello

com.vivo.study.spidemo.spi.impl.TextHello

3.使用 ServiceLoader 来加载配置文件中指定的实现

1

2

3

4

5

6

7

8

packagecom.vivo.study.spidemo.testimport java.util.ServiceLoader;importcom.vivo.study.spidemo.spi.HelloSPI;publicclassSPIDemo {

    publicstaticvoidmain(String[] args) {

        ServiceLoader<HelloSPI> serviceLoader = ServiceLoader.load(HelloSPI.class);        // 执行不同厂商的业务实现,具体根据业务需求配置

        for(HelloSPI helloSPI : serviceLoader) {

            helloSPI.sayHello();

        }

    }

}

输出结果如下:

1

2

Image Hello

Text Hello

四、源码分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者

publicfinalclassServiceLoader<S> implementsIterable<S>

{

    // 查找配置文件的目录

    privatestaticfinalString PREFIX = "META-INF/services/";

    // 表示要被加载的服务的类或接口

    privatefinalClass<S> service;

    // 这个ClassLoader用来定位,加载,实例化服务提供者

    privatefinalClassLoader loader;

    // 访问控制上下文

    privatefinalAccessControlContext acc;

    // 缓存已经被实例化的服务提供者,按照实例化的顺序存储

    privateLinkedHashMap<String,S> providers = newLinkedHashMap<>();

    // 迭代器

    privateLazyIterator lookupIterator; 

}

1

2

3

4

5

6

7

8

9

// 服务提供者查找的迭代器public Iterator<S> iterator() {    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders

            = providers.entrySet().iterator();        // hasNext方法

        publicbooleanhasNext() {            if(knownProviders.hasNext())                returntrue;            returnlookupIterator.hasNext();

        }        // next方法

        publicS next() {            if(knownProviders.hasNext())                returnknownProviders.next().getValue();            returnlookupIterator.next();

        }

    };

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

// 服务提供者查找的迭代器

privateclassLazyIterator implementsIterator<S> {

    // 服务提供者接口

    Class<S> service;

    // 类加载器

    ClassLoader loader;

    // 保存实现类的url

    Enumeration<URL> configs = null;

    // 保存实现类的全名

    Iterator<String> pending = null;

    // 迭代器中下一个实现类的全名

    String nextName = null;

 

    publicbooleanhasNext() {

        if(nextName != null) {

            returntrue;

        }

        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()) {

                returnfalse;

            }

            pending = parse(service, configs.nextElement());

        }

        nextName = pending.next();

        returntrue;

    }

 

    publicS next() {

        if(!hasNext()) {

            thrownewNoSuchElementException();

        }

        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);

            returnp;

        } catch(Throwable x) {

            fail(service, "Provider "+ cn + " could not be instantiated: "+ x, x);

        }

        thrownewError();          // This cannot happen

    }

}

首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。

其次,LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。

后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap类型) 然后返回实例对象。

五、不足

1.不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。

2.获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。

3.多个并发多线程使用 ServiceLoader 类的实例是不安全的。

六、规避

针对以上的不足点,我们在SPI机制选择时,可以考虑使用dubbo实现的SPI机制。

具体参考: http://dubbo.apache.org/zh-cn/blog/introduction-to-dubbo-spi.html

分享好友

分享这个小栈给你的朋友们,一起进步吧。

应用开发
创建时间:2020-06-17 15:31:04
应用软件开发是指使用程序语言C#、java、 c++、vb等语言编写,主要是用于商业、生活应用的软件的开发。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 栈栈
    专家
戳我,来吐槽~