Java SPI 实战

作者: 殷天文 | 来源:发表于2020-08-06 00:22 被阅读0次

    SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制,可以轻松实现面向服务的注册与发现,完成服务提供与使用的解耦,并且可以实现动态加载

    SPI 能做什么

    利用SPI机制,sdk的开发者可以为使用者提供扩展点,使用者无需修改源码,有点类似Spring @ConditionalOnMissingBean 的意思

    动手实现一个SPI

    例如我们要正在开发一个sdk其中有一个缓存的功能,但是用户很可能不想使用我们的缓存实现,用户想要自定义缓存的实现,此时使用spi就非常的合适了

    新建一个maven工程命名为sdk

    image.png

    Cache 接口

    import java.util.ServiceLoader;
    
    public interface Cache {
    
        String getName();
    
        static Cache load() {
            // ServiceLoader 实现了 Iterable,可以加载到Cache接口的多个实现类
            ServiceLoader<Cache> cacheServiceLoader =  ServiceLoader.load(Cache.class);
            return cacheServiceLoader.iterator().next();
        }
    
    }
    
    

    ServiceLoader 是Java提供服务发现工具类,这是我们实现SPI的关键

    CacheDefaultImpl

    public class CacheDefaultImpl implements Cache {
        public String getName() {
            return "defaultImpl";
        }
    }
    

    除此之外,ServiceLoader 还需要在classpath:META-INF/services 下找到以该接口全名命名的文件,这里我们直接在resource 目录下创建META-INF/services/ com.github.tavenyin.Cache文件即可,文件中指定Cache的实现类

    # 此处可以指定多个实现类
    com.github.tavenyin.CacheDefaultImpl
    

    Run

    我们建立一个新的maven子工程,并引入sdk模块,执行测试代码

    System.out.println(Cache.load().getName()) 
    # 输出结果为 defaultImpl
    

    使用者定制化

    那么如果sdk的使用者不想使用我们的CacheDefaultImpl了怎么办,没关系使用者只需要覆盖 classpath:META-INF/services/com.github.tavenyin.Cache 就可以了 (使用者在同样在resource下创建即可覆盖)

    我们再来运行一下测试代码,输出结果为 newImpl

    image.png

    ServiceLoader 实现原理

    ServiceLoader 的实现原理还是比较简单的,试想一下,如果我们自己实现一个ServiceLoader,我们会怎么做?

    1. 通过指定的文件加载出所有的类名
    2. 通过反射构建这些对象

    没错,ServiceLoader 就是这么做的,我们来简单看一下源码

    入口 ServiceLoader::iterator::next

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;   
    
    // ServiceLoader::iterator
    public Iterator<S> iterator() {
        return new Iterator<S>() {
    
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
    
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
    
            // ServiceLoader::iterator::next
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        };
    }
    

    从providers 初始为一个空的LinkedHashMap,我们无需关注,所以knownProviders::hasNext 一定返回false,我们直奔knownProviders::next

    knownProviders::next 中核心逻辑在nextService() 中

    private S nextService() {
        // hasNextService 中做了两件事
        // 1. 判断是否还有服务的提供者
        // 2. 通过 "META-INF/services/" + 接口全名 加载所有提供者ClassName
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // 通过ClassName 创建Class
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            // 反射创建实现类实例
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    

    与我们上述分析的实现过程一致,更多细节感兴趣的童鞋可自行阅读

    ServiceLoader 如何实现动态加载

    同一个 ServiceLoader 对象的话,不会重新加载META-INF/services/下的信息。如果我们需要动态加载的话,可以考虑每次重新创建新的ServiceLoader 对象,或者调用 ServiceLoader::reload

    demo 地址

    https://github.com/TavenYin/java-spi.git

    如果觉得有收获,可以关注我的公众号【殷天文】,你的点赞和关注就是对我最大的支持

    相关文章

      网友评论

        本文标题:Java SPI 实战

        本文链接:https://www.haomeiwen.com/subject/zbqarktx.html