Java的SPI机制

什么是SPI

SPI全称Service Provider Interface,是Java提供的一种接口扩展机制。通过该机制可以将接口的定义与接口的实现分离,实现代码解耦。

使用方式

SPI的使用方法很简单,只需要按如下步骤即可:

  1. 定义接口

    package cn.bdqfork.spi; /** * @author bdq * @since 2020/3/2 */ public interface UserService { void sayHello(); }
  2. 实现接口

    package cn.bdqfork.spi; /** * @author bdq * @since 2020/3/2 */ public class UserServiceImpl implements UserService { @Override public void sayHello() { System.out.println("hello"); } }
  3. 编写扩展文件

    在META-INF/services目录下创建一个以接口全类名命名的文件,即com.test.service.UserService文件。

    - src -main -resources - META-INF - services - cn.bdqfork.spi.UserService

    文件的内容为实现类的全类名。

    cn.bdqfork.spi.UserServiceImpl
  4. 使用ServiceLoader加载服务

    package cn.bdqfork.spi; import java.util.ServiceLoader; /** * @author bdq * @since 2020/3/2 */ public class Main { public static void main(String[] args) { ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); for (UserService userService : userServices) { userService.sayHello(); } } }

SPI的缺点

虽然SPI机制可以很方便的将接口与其实现分离,但是却有两个缺点:

  • 在ServiceLoader加载的时候,会一次性将所有的Service都加载到JVM中,包括并不会用到的一些扩展。
  • 多线程并发访问的时候会有线程问题。

SPI的实现原理

SPI机制的原理实际上不是很难,整个加载扩展服务的过程如下:

  1. 扫描META-INF/services目录下的文件。
  2. 读取文件内容,加载服务实现的Class。
  3. 实例化服务实现。
  4. 返回服务实例。

实现一个简单SPI

了解了SPI的实现原理之后,便可以很简单的实现一个SPI,下面笔者介绍一下笔者实现的一个SPI,参考了Dubbo的SPI实现,但整体原理还是差不多的。

首先定义一个ExtensionLoader类以及相关属性:

/** * SPI扩展 * * @author bdq * @since 2019-08-20 */ public class ExtensionLoader<T> { /** * 扫描路径 */ private static final String PREFIX = "META-INF/extensions/"; /** * 缓存 */ private static final Map<String, ExtensionLoader<?>> CACHES = new ConcurrentHashMap<>(); /** * 扩展Class名称缓存 */ private final Map<Class<T>, String> classNames = new ConcurrentHashMap<>(); /** * 扩展Class缓存 */ private final Map<String, Class<T>> extensionClasses = new ConcurrentHashMap<>(); /** * 扩展实例缓存 */ private volatile Map<String, T> cacheExtensions; /** * 默认扩展名 */ private String defaultName; /** * 扩展服务类型 */ private Class<T> type; private ExtensionLoader(Class<T> type) { this.type = type; } // ...... }

然后实现加载扩展的方法,主要实现了扩展实现类的加载,具体代码如下:

private void loadExtensionClasses() { if (classNames.size() > 0) { return; } try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 加载扩展文件 Enumeration<URL> urlEnumeration = classLoader.getResources(PREFIX + type.getName()); while (urlEnumeration.hasMoreElements()) { URL url = urlEnumeration.nextElement(); if (url.getPath().isEmpty()) { throw new IllegalArgumentException("Extension path " + PREFIX + type.getName() + " don't exsist !"); } // 读取文件内容 if (url.getProtocol().equals("file") || url.getProtocol().equals("jar")) { URLConnection urlConnection = url.openConnection(); Reader reader = new InputStreamReader(urlConnection.getInputStream()); BufferedReader bufferedReader = new BufferedReader(reader); // 逐行读取 String line; while ((line = bufferedReader.readLine()) != null) { if (line.equals("")) { continue; } // 过滤注释 if (line.contains("#")) { line = line.substring(0, line.indexOf("#")); } // 解析key=value String[] values = line.split("="); String name = values[0].trim(); String impl = values[1].trim(); if (extensionClasses.containsKey(name)) { throw new IllegalStateException("Duplicate extension named " + name); } // 加载Class @SuppressWarnings("unchecked") Class<T> clazz = (Class<T>) classLoader.loadClass(impl); // 缓存Class classNames.putIfAbsent(clazz, name); extensionClasses.putIfAbsent(name, clazz); } } } } catch (Exception e) { throw new IllegalArgumentException("Fail to get extension class from " + PREFIX + type.getName() + "!", e); } }

实例化所有的扩展,并缓存到Map中。

/** * 获取所有扩展 * * @return Map<String, T> */ public Map<String, T> getExtensions() { if (cacheExtensions == null) { cacheExtensions = new ConcurrentHashMap<>(); loadExtensionClasses(); for (Map.Entry<String, Class<T>> entry : extensionClasses.entrySet()) { Class<T> clazz = entry.getValue(); T instance; try { instance = clazz.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalStateException(e); } cacheExtensions.putIfAbsent(entry.getKey(), instance); } } return Collections.unmodifiableMap(cacheExtensions); }

提供按名获取扩展和获取默认扩展的方法。

/** * 根据extensionName获取扩展实例 * * @param extensionName 扩展名称 * @return T */ public T getExtension(String extensionName) { T extension = getExtensions().get(extensionName); if (extension != null) { return extension; } throw new IllegalStateException("No extension named " + extensionName + " for class " + type.getName() + "!"); } /** * 根据extensionName获取扩展实例 * * @return T */ public T getDefaultExtension() { T extension = getExtensions().get(defaultName); if (extension != null) { return extension; } throw new IllegalStateException("No default extension named " + defaultName + " for class " + type.getName() + "!"); }

最后提供一个工厂方法,创建ExtensionLoader实例。

/** * 获取扩展接口对应的ExtensionLoader * * @param clazz 扩展接口 * @param <T> Class类型 * @return ExtensionLoader<T> */ @SuppressWarnings("unchecked") public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> clazz) { String className = clazz.getName(); if (!clazz.isInterface()) { throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className + ", class is not Interface !"); } SPI spi = clazz.getAnnotation(SPI.class); if (spi == null) { throw new IllegalArgumentException("Fail to create ExtensionLoader for class " + className + ", class is not annotated by @SPI !"); } ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) CACHES.get(className); if (extensionLoader == null) { CACHES.putIfAbsent(className, new ExtensionLoader<>(clazz)); extensionLoader = (ExtensionLoader<T>) CACHES.get(className); extensionLoader.defaultName = spi.value(); } return extensionLoader; }

SPI注解的定义如下:

/** * 注解在扩展类上,表示可以扩展 * * @author bdq * @since 2019/9/21 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SPI { /** * 默认扩展名 */ String value() default ""; }

以上是一个简单SPI的实现代码,使用方法也很简单,跟JDK的使用方法差不多,区别是需要使用@SPI注解在服务接口上表明扩展需求,且配置文件存储在META-INF/extensions文件夹下,具体使用方法如下。

  1. 首先定义接口。

    package com.github.bdqfork.core.extension; /** * @author bdq * @since 2020/2/22 */ @SPI public interface IExtensionTest { }
  2. 实现接口

    package com.github.bdqfork.core.extension; /** * @author bdq * @since 2020/2/22 */ public class ExtensionTestImpl1 implements IExtensionTest { } package com.github.bdqfork.core.extension; /** * @author bdq * @since 2020/2/22 */ public class ExtensionTestImpl2 implements IExtensionTest { }
  3. 编写配置文件

    在META-INF/services目录下创建一个以接口全类名命名的文件,即com.github.bdqfork.core.extension.ExtensionLoaderTest文件。

    - src -main -resources - META-INF - extensions - com.github.bdqfork.core.extension.IExtensionTest

    文件内容如下:

    imp1=com.github.bdqfork.core.extension.ExtensionTestImpl1 imp2=com.github.bdqfork.core.extension.ExtensionTestImpl2

    这里为每一个扩展实例提供了名称。

  4. 通过ExtensionLoader获取扩展实例

    package com.github.bdqfork.core.extension; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class ExtensionLoaderTest { @Test public void getExtension() { ExtensionLoader<IExtensionTest> extensionLoader = ExtensionLoader.getExtensionLoader(IExtensionTest.class); IExtensionTest iExtensionTest = extensionLoader.getExtension("imp1"); assert iExtensionTest != null; } }

以上就是一个简单SPI的实现,在笔者的ioc容器festival和rpc框架hamal中均使用到了SPI,提供扩展服务,感兴趣的同学欢迎查看笔者的项目。

坚持原创技术分享,您的支持将鼓励我继续创作!
  • 本文作者:bdqfork
  • 本文链接:/articles/55
  • 版权声明:本博客所有文章除特别声明外,均采用BY-NC-SA 许可协议。转载请注明出处!
表情 |预览
快来做第一个评论的人吧~