java spi_JAVA SPI机制详解

一、Java SPI是什么

SPI的英文全称为Service Provider Interface,字面意思为服务提供者接口,它是jdk提供给“服务提供厂商”或者“插件开发者”使用的接口。

在面向对象的设计中,模块之间我们一般会采取面向接口编程的方式,而在实际编程过程过程中,API的实现是封装在jar中,当我们想要换一种实现方法时,还要生成新的jar替换以前的实现类。而通过jdk的SPI机制就可以实现,首先不需要修改原来作为接口的jar的情况下,将原来实现的那个jar替换为另外一种实现的jar即可。

总结一下SPI的思想:在系统的各个模块中,往往有不同的实现方案,例如日志模块的方案、xml解析的方案等,为了在装载模块的时候不具体指明实现类,我们需要一种服务发现机制,java spi就提供这样一种机制。有点类似于IoC的思想,将服务装配的控制权移到程序之外,在模块化设计时尤其重要。

顺便提一下,Java SPI机制在很多大型中间件吗,例如Dubbo中均有采用,属于高级Java开发的进阶必备知识点,务必要求掌握。

二、Java SPI使用规范

定义服务的通用接口,针对通用的服务接口,提供具体的实现类。

在jar包的META-INF/services/目录中,新建一个文件,文件名为 接口的"全限定名"。 文件内容为该接口的具体实现类的"全限定名"。

将spi所在jar放在主程序的classpath中

服务调用方用java.util.ServiceLoader,用服务接口为参数,去动态加载具体的实现类到JVM中。

三、API和SPI的区别

API:提供给调用方,完成某项功能的接口(类、或者方法),你可以使用它完成任务。

SPI:是一种callback的思想,在一些通用的标准中(即API),为实现厂商提供扩展点。当API被调用时,会动态加载SPI路由到特定的实现中。

四、Java SPI 的典型运用场景

案例一:

java.sql.Driver的spi实现,有mysql驱动、oracle驱动等。以mysql为例,实现类是com.mysql.jdbc.Driver,在mysql-connector-java-5.1.6.jar中,我们可以看到有一个META-INF/services目录,目录下有一个文件名为java.sql.Driver的文件,其中的内容是com.mysql.jdbc.Driver。

案例二:

举一个典型的案例:

25b3559ecc47

image.png

slf4j是一个典型的门面接口,早起我们使用log4j作为日记记录框架,我们需要同时引入slf4j和log4j的依赖。后面比较流行logback,我们也想要把项目切换到logback上来,此时利用SPI的机制,我们只需要把log4j的jar包替换为logback的jar包就可以了

五、Java SPI Demo

该示例主要为了展示如何使用SPI,接口是数字操作接口,普通的API的实现类是加法操作;两个SPI实现类分别是减法操作和乘法操作。程序结构如下图:

25b3559ecc47

image.png

INumOperate接口的代码如下:

package com.example.demo.operation;

/**

* @Description 数字操作接口

* @Author louxiujun

* @Date 2019/11/7 14:09

**/

public interface INumOperate {

int operate(int a, int b);

}

普通的api实现,加法操作,代码如下:

package com.example.demo.operation.api;

import com.example.demo.operation.INumOperate;

/**

* @Description 数字相加

* @Author louxiujun

* @Date 2019/11/7 14:09

**/

public class NumPlusOperateImpl implements INumOperate {

@Override

public int operate(int a, int b) {

int r = a + b;

System.out.println("[实现类机制]加法,结果:" + r);

return r;

}

}

实现乘法的spi,在语法结构上和普通api实现一模一样,如下

package com.example.demo.operation.spi;

import com.example.demo.operation.INumOperate;

/**

* @Description 数字相乘

* @Author louxiujun

* @Date 2019/11/7 14:10

**/

public class NumMutliOperateImpl implements INumOperate {

@Override

public int operate(int a, int b) {

int r = a * b;

System.out.println("[SPI机制]乘法,结果:" + r);

return r;

}

}

实现减法的spi,在语法结构上和普通api实现一模一样,如下

package com.example.demo.operation.spi;

import com.example.demo.operation.INumOperate;

/**

* @Description 数字相减

* @Author louxiujun

* @Date 2019/11/7 14:10

**/

public class NumSubtractOperateImpl implements INumOperate {

@Override

public int operate(int a, int b) {

int r = a - b;

System.out.println("[SPI机制]减法,结果:" + r);

return r;

}

}

在resources目录下,新建META-INFO目录,再在其下面新建services目录,新建一个以com.example.demo.operation.INumOperate命名的文件,名称来自于接口的全限定路径,文件内容指明两个SPI的实现类的全限定名称,具体内容如下:

com.example.demo.operation.spi.NumMutliOperateImpl

com.example.demo.operation.spi.NumSubtractOperateImpl

下面我们在test目录下新建一个名为INumOperateTest单元测试类:

package com.example.demo.operation;

import com.example.demo.BaseTest;

import com.example.demo.operation.api.NumPlusOperateImpl;

import org.junit.Test;

import java.util.Iterator;

import java.util.ServiceLoader;

/**

* @Description

* @Author louxiujun

* @Date 2019/11/7 14:14

**/

public class INumOperateTest extends BaseTest {

private int num1 = 9;

private int num2 = 3;

@Test

public void testOperate() {

// 普通的实现类机制,加法

INumOperate plus = new NumPlusOperateImpl();

plus.operate(num1, num2);

// SPI机制,寻找所有的实现类,顺序执行

ServiceLoader loader = ServiceLoader.load(INumOperate.class); // 查找SPI实现类,并加载到jvm

Iterator iter = loader.iterator();

while (iter.hasNext()) {

INumOperate op = iter.next();

op.operate(num1, num2);

}

}

}

其中,单元测试的基类如下:

package com.example.demo;

import org.junit.runner.RunWith;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)

@SpringBootTest

public class BaseTest {

}

测试输出结果如下:

[实现类机制]加法,结果:12

[SPI机制]乘法,结果:27

[SPI机制]减法,结果:6

踩坑:resources下的META-INF/resources目录不能一次性建成,需要逐级建出来。详细的踩坑说明参考: Inteilj IDEA多级目录生成踩坑记

六、项目实战

下面举一个实际生产环境中使用到的使用Java SPI机制实现。先说一下需求背景,一个项目,之前部署在环境A下面(集团内部环境),使用了较多的集团内部中间件,简直可以说是中间件全家桶,使用的时候很爽,后面有了私有化部署的需求,私有化的场景下可没有集团的火力支持,只能使用业界开源方案作为替代品,这个环境就是环境B。由此对此抽象出来的区别如下表所示,无论是消息队列中间件,还是缓存中间件,还是某个下游的元数据服务,都不一样。

中间件/服务

环境A

环境B

消息队列

mq1

mq2

缓存

cache1

cache2

元数据服务

service1

service2

问题来了,怎么解决环境下部署的下游依赖的问题呢?如果只是某个参数不一样,可能通过一个配置中心下发一下配置参数就可以了,但是这里涉及到的是下游完整的业务模块,不是几行代码,也不是几个文件能够搞定的事情,很有可能一个十几万行代码的依赖包。怎么办呢?

为了在不同的部署环境下,使用对应的中间件/服务,我们有以下几种方案:

方案

优点

缺点

不同环境部署不同的应用

1、简单、代码逻辑上不用区分环境

2、代码修改只影响所属应用

1、功能同步升级麻烦

2、多个应用多套代码维护成本高

代码IF,ELSE判断执行对应环境的逻辑

1、一套代码支持多个部署环境

2、同步升级,统一维护,降低成本

不符合"开闭原则",当需要支持更多环境时,需要修改已有代码,风险大

SPI 插件式开发

1、一套代码支持多个部署环境

2、同步升级、维护成本低

3、当需要支持更多环境时,只需要实现对应的服务接口,替换服务插件即可

实现复杂

通过比较以上三种方案,我们决定使用"SPI插件式开发"的方式支持"一套代码多环境部署"的功能。

pom文件:

environment1

true

com.alibaba.work.demo

environment1

environment2

false

com.alibaba.work.demo

environment2

SPI加载类ServiceLoaderContainer,用于保存类及其对应的类加载器:

public class ServiceLoaderContainer {

private static Map, Object> container = new HashMap, Object>();

@SuppressWarnings( {"unchecked", "unused"} )

protected T getService(Class cls) {

T obj = (T) container.get(cls);

if (obj != null) {

return obj;

}

synchronized (this) {

// 并行场景下 当前线程上下文类加载器有可能为null

ClassLoader cl = Thread.currentThread().getContextClassLoader();

if (cl == null) {

cl = this.getClass().getClassLoader();

}

ServiceLoader loaders = ServiceLoader.load(cls, cl);

for (T loader : loaders) {

container.put(cls, loader);

return loader;

}

}

throw new RuntimeException(e.getMessage());

}

}

使用:

@service

public class MyRoleServiceImpl extends ServiceLoaderContainer implments MyRoleService{

protected RoleService getRoleService() {

return getService(RoleService.class);

}

...

}

RoleServiceImpl有两个不同的实现,分别在包environment1和environment2中,使用的时候只需要使用maven -P 指定编译打包时需要使用到的jar包依赖,在运行时通过ServiceLoaderContainer调用ServiceLoader loaders = ServiceLoader.load(cls, cl);加载接口类RoleService的具体的类实现。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值