Java SPI机制详解

什么是SPI?

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

类图中,接口对应定义的抽象SPI接口;实现方实现SPI接口;调用方依赖SPI接口。

SPI接口的定义在调用方,在概念上更依赖调用方;组织上位于调用方所在的包中;实现位于独立的包中。

当接口属于实现方的情况,实现方提供了接口和实现,这个用法很常见,属于API调用。我们可以引用接口来达到调用某实现类的功能。

Java SPI 应用实例

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

SPI接口

1public interface ObjectSerializer {
2
3    byte[] serialize(Object obj) throws ObjectSerializerException;
4
5    <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;
6
7    String getSchemeName();
8}

定义了一个对象序列化接口,内有三个方法:序列化方法、反序列化方法和序列化名称。

SPI具体实现

 1public class KryoSerializer implements ObjectSerializer {
 2
 3    @Override
 4    public byte[] serialize(Object obj) throws ObjectSerializerException {
 5        byte[] bytes;
 6        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 7        try {
 8            //获取kryo对象
 9            Kryo kryo = new Kryo();
10            Output output = new Output(outputStream);
11            kryo.writeObject(output, obj);
12            bytes = output.toBytes();
13            output.flush();
14        } catch (Exception ex) {
15            throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
16        } finally {
17            try {
18                outputStream.flush();
19                outputStream.close();
20            } catch (IOException e) {
21
22            }
23        }
24        return bytes;
25    }
26
27    @Override
28    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
29        T object;
30        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
31            Kryo kryo = new Kryo();
32            Input input = new Input(inputStream);
33            object = kryo.readObject(input, clazz);
34            input.close();
35        } catch (Exception e) {
36            throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
37        }
38        return object;
39    }
40
41    @Override
42    public String getSchemeName() {
43        return "kryoSerializer";
44    }
45
46}

使用Kryo的序列化方式。Kryo 是一个快速高效的Java对象图形序列化框架,它原生支持java,且在java的序列化上甚至优于google著名的序列化框架protobuf。

 1public class JavaSerializer implements ObjectSerializer {
 2    @Override
 3    public byte[] serialize(Object obj) throws ObjectSerializerException {
 4        ByteArrayOutputStream arrayOutputStream;
 5        try {
 6            arrayOutputStream = new ByteArrayOutputStream();
 7            ObjectOutput objectOutput = new ObjectOutputStream(arrayOutputStream);
 8            objectOutput.writeObject(obj);
 9            objectOutput.flush();
10            objectOutput.close();
11        } catch (IOException e) {
12            throw new ObjectSerializerException("JAVA serialize error " + e.getMessage());
13        }
14        return arrayOutputStream.toByteArray();
15    }
16
17    @Override
18    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
19        ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(param);
20        try {
21            ObjectInput input = new ObjectInputStream(arrayInputStream);
22            return (T) input.readObject();
23        } catch (IOException | ClassNotFoundException e) {
24            throw new ObjectSerializerException("JAVA deSerialize error " + e.getMessage());
25        }
26    }
27
28    @Override
29    public String getSchemeName() {
30        return "javaSerializer";
31    }
32
33}

Java原生的序列化方式。

增加META-INF目录文件

Resource下面创建META-INF/services 目录里创建一个以服务接口命名的文件

1com.blueskykong.javaspi.serializer.KryoSerializer
2com.blueskykong.javaspi.serializer.JavaSerializer

Service类

 1@Service
 2public class SerializerService {
 3
 4
 5    public ObjectSerializer getObjectSerializer() {
 6        ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);
 7
 8        final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
 9                .findFirst();
10
11        return serializer.orElse(new JavaSerializer());
12    }
13}

获取定义的序列化方式,且只取第一个(我们在配置中写了两个),如果找不到则返回Java原生序列化方式。

测试类

 1    @Autowired
 2    private SerializerService serializerService;
 3
 4    @Test
 5    public void serializerTest() throws ObjectSerializerException {
 6        ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
 7        System.out.println(objectSerializer.getSchemeName());
 8        byte[] arrays = objectSerializer.serialize(Arrays.asList("1", "2", "3"));
 9        ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
10        Assert.assertArrayEquals(Arrays.asList("1", "2", "3").toArray(), list.toArray());
11    }

测试用例通过,且输出kryoSerializer

SPI的用途

数据库DriverManager、Spring、ConfigurableBeanFactory等都用到了SPI机制,这里以数据库DriverManager为例,看一下其实现的内幕。

DriverManager是jdbc里管理和注册不同数据库driver的工具类。针对一个数据库,可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时,不希望修改现有的代码,而希望通过一个简单的配置就可以达到效果。 在使用mysql驱动的时候,会有一个疑问,DriverManager是怎么获得某确定驱动类的?我们在运用Class.forName("com.mysql.jdbc.Driver")加载mysql驱动后,就会执行其中的静态代码把driver注册到DriverManager中,以便后续的使用。

在JDBC4.0之前,连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要Class.forName来加载驱动,直接获取连接即可,这里使用了Java的SPI扩展机制来实现。

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

mysql

在mysql-connector-java-5.1.45.jar中,META-INF/services目录下会有一个名字为java.sql.Driver的文件:

1com.mysql.jdbc.Driver
2com.mysql.fabric.jdbc.FabricMySQLDriver

pg

而在postgresql-42.2.2.jar中,META-INF/services目录下会有一个名字为java.sql.Driver的文件:

1org.postgresql.Driver

用法

1String url = "jdbc:mysql://localhost:3306/test";
2Connection conn = DriverManager.getConnection(url,username,password);

上面展示的是mysql的用法,pg用法也是类似。不需要使用Class.forName("com.mysql.jdbc.Driver")来加载驱动。

Mysql DriverManager实现

上面代码没有了加载驱动的代码,我们怎么去确定使用哪个数据库连接的驱动呢?这里就涉及到使用Java的SPI扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,在DriverManager中有一个静态代码块如下:

1static {
2    loadInitialDrivers();
3    println("JDBC DriverManager initialized");
4}

可以看到其内部的静态代码块中有一个loadInitialDrivers方法,loadInitialDrivers用法用到了上文提到的spi工具类ServiceLoader:

 1    public Void run() {
 2
 3        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
 4        Iterator<Driver> driversIterator = loadedDrivers.iterator();
 5
 6        /* Load these drivers, so that they can be instantiated.
 7         * It may be the case that the driver class may not be there
 8         * i.e. there may be a packaged driver with the service class
 9         * as implementation of java.sql.Driver but the actual class
10         * may be missing. In that case a java.util.ServiceConfigurationError
11         * will be thrown at runtime by the VM trying to locate
12         * and load the service.
13         *
14         * Adding a try catch block to catch those runtime errors
15         * if driver not available in classpath but it's
16         * packaged as service and that service is there in classpath.
17         */
18        try{
19            while(driversIterator.hasNext()) {
20                driversIterator.next();
21            }
22        } catch(Throwable t) {
23        // Do nothing
24        }
25        return null;
26    }

遍历使用SPI获取到的具体实现,实例化各个实现类。在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。

总结

SPI机制在实际开发中使用得场景也有很多。特别是统一标准的不同厂商实现,当有关组织或者公司定义标准之后,具体厂商或者框架开发者实现,之后提供给开发者使用。

本文代码: https://github.com/keets2012/Spring-Boot-Samples/tree/master/java-spi

参考

  1. https://cxis.me/2017/04/17/Java%E4%B8%ADSPI%E6%9C%BA%E5%88%B6%E6%B7%B1%E5%85%A5%E5%8F%8A%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
  2. https://zhuanlan.zhihu.com/p/28909673

原文发布于微信公众号 - aoho求索(aohoBlog)

原文发表时间:2018-05-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏owent

我们的Lua类绑定机制

最近一个人搞后台,框架底层+逻辑功能茫茫多,扛得比较辛苦,一直没抽出空来写点东西。

3031
来自专栏Golang语言社区

GO语言标准库概览

在Go语言五周系列教程的最后一部分中,我们将带领大家一起来浏览一下Go语言丰富的标准库。 Go标准库包含了大量包,提供了丰富广泛的功能特性。这里提供了概览仅仅是...

38710
来自专栏nnngu

百度搜索 “Java面试题” 前200页(面试必看)

本文中的题目来源于网上的一篇文章《百度搜索 “Java面试题” 前200页》,但该文章里面只有题目,没有答案。因此,我整理了一些答案发布于本文。本文整理答案的原...

81711
来自专栏java思维导图

Java 10 已发布!时隔 6 月带来 109 项新特性

关键时刻,第一时间送达! 期待已久,没有跳票的 Java 10 已正式发布! ? 为了更快地迭代,以及跟进社区反馈,Java 的版本发布周期变更为了每六个月一次...

2967
来自专栏JackieZheng

Hadoop阅读笔记(七)——代理模式

  关于Hadoop已经小记了六篇,《Hadoop实战》也已经翻完7章。仔细想想,这么好的一个框架,不能只是流于应用层面,跑跑数据排序、单表链接等,想得其精髓,...

21710
来自专栏Java 技术分享

Struts2 转换器

1252
来自专栏深度学习之tensorflow实战篇

mongodb11天之屠龙宝刀(六)mapreduce:mongodb中mapreduce原理与操作案例

mongodb11天之屠龙宝刀(六)mapreduce:mongodb中mapreduce原理与操作案例 一 Map/Reduce简介 MapReduc...

3996
来自专栏LanceToBigData

OOAD-设计模式(四)结构型模式之适配器、装饰器、代理模式

前言   前面我们学习了创建型设计模式,其中有5中,个人感觉比较重要的是工厂方法模式、单例模式、原型模式。接下来我将分享的是结构型模式! 一、适配器模式 1.1...

2029
来自专栏Spark生态圈

[Spark SQL] 主要执行流程

SparkSql的第一件事就是把SQLText解析成语法树,这棵树包含了很多节点对象,节点可以有特定的数据类型,同时可以有0个或者多个子节点,节点在SparkS...

3211
来自专栏一名合格java开发的自我修养

Strom序列化机制

  Storm 中的 tuple可以包含任何类型的对象。由于Storm 是一个分布式系统,所以在不同的任务之间传递消息时Storm必须知道怎样序列化、反序列化消...

902

扫码关注云+社区

领取腾讯云代金券