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 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 {
    /**
     * 扫描路径
     */
    private static final String PREFIX = "META-INF/extensions/";
    /**
     * 缓存
     */
    private static final Map> CACHES = new ConcurrentHashMap<>();
    /**
     * 扩展Class名称缓存
     */
    private final Map, String> classNames = new ConcurrentHashMap<>();
    /**
     * 扩展Class缓存
     */
    private final Map> extensionClasses = new ConcurrentHashMap<>();
    /**
     * 扩展实例缓存
     */
    private volatile Map cacheExtensions;
    /**
     * 默认扩展名
     */
    private String defaultName;
    /**
     * 扩展服务类型
     */
    private Class type;

    private ExtensionLoader(Class type) {
        this.type = type;
    }
    // ......
}

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

    private void loadExtensionClasses() {
        if (classNames.size() > 0) {
            return;
        }
        try {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

            // 加载扩展文件
            Enumeration 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 clazz = (Class) 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
     */
    public Map getExtensions() {
        if (cacheExtensions == null) {
            cacheExtensions = new ConcurrentHashMap<>();
            loadExtensionClasses();

            for (Map.Entry> entry : extensionClasses.entrySet()) {
                Class 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    Class类型
     * @return ExtensionLoader
     */
    @SuppressWarnings("unchecked")
    public static  ExtensionLoader getExtensionLoader(Class 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 extensionLoader = (ExtensionLoader) CACHES.get(className);

        if (extensionLoader == null) {
            CACHES.putIfAbsent(className, new ExtensionLoader<>(clazz));
            extensionLoader = (ExtensionLoader) 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 extensionLoader = ExtensionLoader.getExtensionLoader(IExtensionTest.class);
            IExtensionTest iExtensionTest = extensionLoader.getExtension("imp1");
            assert iExtensionTest != null;
        }
    
    }
    

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

你可能感兴趣的:(Java的SPI机制)