java-spi

为什么要使用spi

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。

spi的概念

首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。

先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的spi,它具有以下特征:

  1. 概念上更接近实现方
  2. 组织上位于实现方所在的包中
  3. 实现和接口在一个包中

当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:

  1. 概念上更依赖调用方
  2. 组织上位于调用方所在的包中
  3. 实现位于独立的包中(也可认为在提供方中)

如下图所示: 

开源的案例

接下来从几个案例总结下java spi思想

Jdk

在jdk6里面引进的一个新的特性ServiceLoader,从官方的文档来说,它主要是用来装载一系列的servive provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 可能上面讲的有些抽象,下面就结合一个示例来具体讲讲。

DriverManager spi案例

DriverManager是jdbc里管理和注册不同数据库driver的工具类。从它涉及的初衷来看,和我们前面讨论的场景有相似之处。针对一个数据库,可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时,不希望修改现有的代码,而希望通过一个简单的配置就可以达到效果。

我们再使用mysql驱动的时候,会有一个疑问,DriverManager是怎么获得某确定驱动类的?

我们再运用Class.forName("com.mysql.jdbc.Driver")加载mysql驱动后,就会执行其中的静态代码把driver注册DriverManager中,以便后续的使用。 代码如下:

package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

这里可以看到,不同的驱动实现了相同的的接口java.spi.Driver,然后通过registerDriver把当前driver加载到DriverManager中,这就体现了使用方提供规则,提供方根据规则则把自己加载到使用放中的spi思想。

这里有个有趣的地方,查看DriverManager的源码,可以看到其内部的静态代码块中有一个loadInitialDrivers方法,在注释中我们看到用到了上文提到的spi工具类ServiceLoader

/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
	loadInitialDrivers();
	println("JDBC DriverManager initialized");
}

点进方法,看到方法里有如下代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> drivers = loadedDrivers.iterator();
println("DriverManager.initialize: jdbc.drivers = " + loadedDrivers);

可见,DriverManager初始化时也运用了spi的思想,使用ServiceLoader把写到配置文件里的Driver都加载进来了。

我们打开mysql-connector-java的jar包,果然在META-INF/services下发现了上文中提到的接口路径,打开里面的内容,可以看到是com.mysql.jdbc.Driver

其实对符合DriverManager设定规则的驱动,我们并不用去调用class.forname,直接连接就好,因为DriverManager在初始化的时候已经把所有符合驱动都加在进去了,避免了在程序中频繁加载。

但对于没有符合配置文件规则的驱动,如oracle,它还是需要去显示调用classforname,再执行静态代码块把驱动加载到manager里,因为它不符合配置文件规则: 

最后总结一下jdk spi需要遵循的规范

Spring

Spring 中运用到spi思想的地方也有很多,下面随便列举几个

scan

我们在spring中可以通过component-scan标签来对指定包路径进行扫描,只要扫到spring制定的@service,@controller等注解,spring自动会把它注入容器。

这就相当于spring制定了注解规范,我们按照这个注解规范开发响应的实现类或controller,spring并不需要感知我们是怎么实现的,他只需要根据注解规范和scan标签注入响应的bean,这正是spi理念的体现。

scope

spring中有作用域scope的概念。 除了singleton、prototype、request、session等spring为我们提供的域,我们还可以自定义scope。

像是自定义一个 ThreadScope实现Scope接口 再把它注册到beanFactory中

Scope threadScope = new ThreadScope();
beanFactory.registerScope("thread", threadScope);

接着就能在xml中使用了

<bean id=".." class=".." scope="thread"/>

spring作为使用方提供了自定义scope的规则,提供方根据规则进行编码和配置,这样在spring中就能运用我们自定义的scope了,并不需要spring感知我们scope里的实现,这也是平台使用方制定规则,提供方负责实现的思想。

总结

其实这里就可以发现,只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值