暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Java SPI 的机制,中高级开发必懂!

搬运工来架构 2019-02-25
495
点击上方蓝色字关注我们~

〓概念

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制

Wikipedia对其的解释

Service Provider Interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.

SPI是一个API,它是由第三方实现或扩展的。它可以用于支持框架扩展和可替换组件。

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

服务提供者接口(SPI)是服务定义的公共接口和抽象类的集合。SPI 定义了应用程序可用的类和方法。 服务提供者实现 SPI。

具有可扩展服务的应用程序将允许供应商甚至客户在不修改原始应用程序的情况下添加服务提供者。

应用示例

当服务的提供者提供了一种接口的实现之后,需要在 classpath 下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类

当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的 META-INF/services/ 中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。

JDK中查找服务实现的工具类是:java.util.ServiceLoader

■SPI 接口

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

1public interface ObjectSerializer {
2   byte[] serialize(Object obj) throws Exception;
3   String getSchemeName();
4}

■SPI 具体实现

1.Kryo 实现

 1public class KryoSerializer implements ObjectSerializer {
2   @Override
3   public byte[] serialize(Object obj) throws Exception {
4       byte[] bytes;
5       ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
6       try {
7           //获取kryo对象
8           Kryo kryo = new Kryo();
9           Output output = new Output(outputStream);
10           kryo.writeObject(output, obj);
11           bytes = output.toBytes();
12           output.flush();
13       }
14       catch (Exception ex) {
15           throw new Exception("kryo serialize error" + ex.getMessage());
16       }
17       finally {
18           try {
19               outputStream.flush();
20               outputStream.close();
21           }
22           catch (IOException e) {
23           }
24       }
25       return bytes;
26   }
27   @Override
28   public String getSchemeName() {
29       return "kryoSerializer";
30   }
31}


2.Java 原生实现

 1public class JavaSerializer implements ObjectSerializer {
2   @Override
3   public byte[] serialize(Object obj) throws Exception {
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       }
12       catch (IOException e) {
13           throw new Exception("JAVA serialize error " + e.getMessage());
14       }
15       return arrayOutputStream.toByteArray();
16   }
17   @Override
18   public String getSchemeName() {
19       return "javaSerializer";
20   }
21}

■增加配置文件

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

1KryoSerializer
2JavaSerializer

■测试类

 1public class Test {
2   public static void main(String[] args) {
3       List<String> list = Arrays.asList("1""2""3");
4       ServiceLoader<ObjectSerializer> serviceLoader = ServiceLoader.load(ObjectSerializer.class);
5       Iterator<ObjectSerializer> iterator = serviceLoader.iterator();
6       iterator.forEachRemaining(serializer -> {
7           try {
8               System.out.println(serializer.getSchemeName() + ":" + Arrays.toString(serializer.serialize(list)));
9           }
10           catch (Exception e) {
11               e.printStackTrace();
12           }
13       });
14   }
15}

SPI 思想的应用

■数据库驱动加载

在访问数据库时,需要根据数据库类型加载不同的数据库驱动,之前往往都需要通过Class.forName显示加载,从 JDBC 4.0 开始,DriverManager中使用了 SPI 机制,在类加载时会通过 SPI 加载所有的java.sql.Driver接口的实现类,在程序中直接调用 DriverManager.getConnection()方法就可以获取数据库连接。主要代码在loadInitialDrivers方法中

 1private static void loadInitialDrivers() {
2   ...
3   AccessController.doPrivileged(new PrivilegedAction<Void>() {
4       public Void run() {
5           ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
6           //获取迭代器
7           Iterator<Driver> driversIterator = loadedDrivers.iterator();
8           try{
9               //遍历
10               while(driversIterator.hasNext()) {
11                   driversIterator.next();
12                   //可以做具体的业务逻辑
13               }
14           } catch(Throwable t) {
15           // Do nothing
16           }
17           return null;
18       }
19   });
20   ...
21}

注:OJDBC 驱动没有提供 SPI 实现机制

■日志门面接口实现类加载

SLF4J 通过 SPI 机制加载不同提供商的日志实现类,主要代码如下

 1//org.slf4j.LoggerFactory 1.8.0-beta0
2private static List<SLF4JServiceProvider> findServiceProviders() {
3   ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
4   List<SLF4JServiceProvider> providerList = new ArrayList();
5   Iterator i$ = serviceLoader.iterator();
6
7   while(i$.hasNext()) {
8       SLF4JServiceProvider provider = (SLF4JServiceProvider)i$.next();
9       providerList.add(provider);
10   }
11
12   return providerList;
13}

logback 1.3.0-alpha0中有对应的配置文件

注:slf4j 1.7.2 没有使用此机制,1.8.0-beta0 有使用

■Eclipse 插件

Eclipse 使用 OSGi 作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。

一般来说,插件的文件结构必须在指定目录下包含以下三个文件:

  • META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等

  • build.properties: 项目的编译配置信息,包括,源代码路径、输出路径

  • plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等

当 eclipse 启动时,会遍历 plugins 文件夹中的目录,扫描每个插件的清单文件 MANIFEST.MF,并建立一个内部模型来记录它所找到的每个插件的信息,就实现了动态添加新的插件。

■Spring SPI 机制

通过 SpringFactoriesLoader 代替 JDK 中 ServiceLoader,通过 META-INF/spring.factories 文件代替 META-INF/service 目录下的描述文件,具体实现步骤不同,但原理类似。

■Dubbo SPI 扩展

Dubbo 的扩展机制和 Java 的 SPI 机制非常相似,Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,可以加载指定的实现类。

Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,通过键值对的方式进行配置,接口上需要有 @SPI 注解。

〓小结

使用 Java SPI 机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。

应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

虽然 ServiceLoader 也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。此外获取某个实现类的方式也不够灵活。


作者:技术分享tech

来源:https://mp.weixin.qq.com/s/EcRqMUqJocP1LNg1T3Tm6A



推荐阅读



-关注搬运工来架构,与优秀的你一同进步-

文章转载自搬运工来架构,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论