文章目录
什么是SPI?
SPI 即 Service Provider Interface,
字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI
将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。很多框架都使用了Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
SPI 和 API 有什么区别?
说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H
公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
Java SPI 应用实例
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务实现的工具类是:java.util.ServiceLoader。
SPI接口
public interface ObjectSerializer {
byte[] serialize(Object obj) throws ObjectSerializerException;
<T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;
String getSchemeName();
}
定义了一个对象序列化接口,内有三个方法:序列化方法、反序列化方法和序列化名称。
SPI具体实现
public class KryoSerializer implements ObjectSerializer {
@Override
public byte[] serialize(Object obj) throws ObjectSerializerException {
byte[] bytes;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
//获取kryo对象
Kryo kryo = new Kryo();
Output output = new Output(outputStream);
kryo.writeObject(output, obj);
bytes = output.toBytes();
output.flush();
} catch (Exception ex) {
throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
} finally {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
}
}
return bytes;
}
@Override
public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
T object;
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
Kryo kryo = new Kryo();
Input input = new Input(inputStream);
object = kryo.readObject(input, clazz);
input.close();
} catch (Exception e) {
throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
}
return object;
}
@Override
public String getSchemeName() {
return "kryoSerializer";
}
}
使用Kryo的序列化方式。Kryo 是一个快速高效的Java对象图形序列化框架,它原生支持java,且在java的序列化上甚至优于google著名的序列化框架protobuf。
public class JavaSerializer implements ObjectSerializer {
@Override
public byte[] serialize(Object obj) throws ObjectSerializerException {
ByteArrayOutputStream arrayOutputStream;
try {
arrayOutputStream = new ByteArrayOutputStream();
ObjectOutput objectOutput = new ObjectOutputStream(arrayOutputStream);
objectOutput.writeObject(obj);
objectOutput.flush();
objectOutput.close();
} catch (IOException e) {
throw new ObjectSerializerException("JAVA serialize error " + e.getMessage());
}
return arrayOutputStream.toByteArray();
}
@Override
public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(param);
try {
ObjectInput input = new ObjectInputStream(arrayInputStream);
return (T) input.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new ObjectSerializerException("JAVA deSerialize error " + e.getMessage());
}
}
@Override
public String getSchemeName() {
return "javaSerializer";
}
}
Java原生的序列化方式。
增加META-INF目录文件
Resource下面创建META-INF/services 目录里创建一个以服务接口命名的文件
com.blueskykong.javaspi.serializer.KryoSerializer
com.blueskykong.javaspi.serializer.JavaSerializer
Service类
@Service
public class SerializerService {
public ObjectSerializer getObjectSerializer() {
ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);
final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
.findFirst();
return serializer.orElse(new JavaSerializer());
}
}
获取定义的序列化方式,且只取第一个(我们在配置中写了两个),如果找不到则返回Java原生序列化方式。
测试类
@Autowired
private SerializerService serializerService;
@Test
public void serializerTest() throws ObjectSerializerException {
ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
System.out.println(objectSerializer.getSchemeName());
byte[] arrays = objectSerializer.serialize(Arrays.asList("1", "2", "3"));
ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
Assert.assertArrayEquals(Arrays.asList("1", "2", "3").toArray(), list.toArray());
}
测试用例通过,且输出kryoSerializer。
总结
SPI机制在实际开发中使用得场景也有很多。特别是统一标准的不同厂商实现,当有关组织或者公司定义标准之后,具体厂商或者框架开发者实现,之后提供给开发者使用。
但是 SPI 机制也存在一些缺点,比如:
1.遍历加载所有的实现类,这样效率还是相对较低的;
2.当多个 ServiceLoader 同时 load 时,会有并发问题。
什么是spring SPI
Spring SPI沿用了JavaSPI的设计思想,Spring采用的是spring.factories方式实现SPI机制,可以在不修改Spring源码的前提下,提供Spring框架的扩展性。
spring SPI如何用
看咋用之前先让我们将眼光汇聚到这个类 SpringFactoriesLoader。
这个类是用于Spring框架内部的一般用途的工厂加载机制的。主要有loadFactories()和loadFactoryNames()这两个公有方法,loadFactories()是加载和实例化资源文件(META-INF/spring.factories)中的对应的工厂的实现类列表,loadFactoryNames()是加载资源文件中对应的工厂的实现类的全路径名称列表。
Spring factories SPI是一个spring.factories配置文件存放多个接口及其对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅spring.factories一个配置文件。
spring SPI 示例
定义接口
public interface DataBaseSPI
{
void getConnection();
}
相关实现
#DB2实现
public class DB2DataBase implements DataBaseSPI
{
@Override
public void getConnection()
{
System.out.println("this database is db2");
}
}
#Mysql实现
public class MysqlDataBase implements DataBaseSPI
{
@Override
public void getConnection()
{
System.out.println("this is mysql database");
}
}
1.在项目的META-INF目录下,新增spring.factories文件
2.填写相关的接口信息,内容如下:
com.skywares.fw.juc.springspi.DataBaseSPI = com.skywares.fw.juc.springspi.DB2DataBase, com.skywares.fw.juc.springspi.MysqlDataBase
说明:多个实现采用逗号分隔。
相关测试类
public class SpringSPITest
{
public static void main(String[] args)
{
List<DataBaseSPI> dataBaseSPIs =SpringFactoriesLoader.loadFactories(DataBaseSPI.class,
Thread.currentThread().getContextClassLoader());
for(DataBaseSPI datBaseSPI:dataBaseSPIs){
datBaseSPI.getConnection();
}
}
}
输出结果
总结
从示例中我们看出,Spring 采用spring.factories实现SPI与java实现SPI非常相似,
但是spring的spi方式针对java的spi进行的相关优化具体内容如下:
Java SPI是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在services目录下;
Spring factories SPI是一个spring.factories配置文件存放多个接口及对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅spring.factories一个配置文件。