美文网首页Java
Java SPI 简介

Java SPI 简介

作者: AlienPaul | 来源:发表于2021-04-04 18:32 被阅读0次

    简介

    SPI全称为Service Provider Interface。是一种为项目留下扩展点的非常好的方式。我们事先在项目中留下Service的接口。通过配置文件就可以控制生成哪些具体的Service实现类。

    一个例子

    我们创建一个maven项目。创建一个Service接口:

    public interface MyService {
        void service();
    }
    

    还有两个MyService的实现类:

    public class MyServiceA implements MyService {
        @Override
        public void service() {
            System.out.println("MyService A");
        }
    }
    

    public class MyServiceB implements MyService {
        @Override
        public void service() {
            System.out.println("MyService B");
        }
    }
    

    根据SPI约定,决定如何生成实现类的配置文件位于classpathMETA-INF/services目录。配置文件的名称为Service接口的全名,包含包名和接口名称。例如包名为com.paul,那么配置文件名为com.paul.MyService。文件的内容只需要将需要创建出的实现类全名列出。比如文件内容为:

    com.paul.MyServiceA
    com.paul.MyServiceB
    

    程序会创建出MyService两个实现类:MyServiceAMyServiceB

    接下来是加载实现类的方法,JDK提供的写法非常的简洁:

    public class MyServiceLoader {
        public static void main(String[] args) {
            ServiceLoader<MyService> myServices = ServiceLoader.load(MyService.class);
            for (MyService myService : myServices) {
                myService.service();
            }
        }
    }
    

    通过ServiceLoaderload方法,创建出一个ServiceLoader对象。迭代这个对象就能够获取所有的实现类。

    上面的例子中服务接口和实现类在同一个项目中。实际上不同服务的实现类可以在不同的maven子项目中,配合与之对应的配置文件,打成jar包发布。项目部署的时候,在classpath放入不同的jar包,服务调用端不需要修改任何代码,就能够更改服务的行为,从而实现应用功能的扩展,非常的方便。

    源代码分析

    想明白SPI的实现方式,阅读源代码是免不了的。

    我们从上面例子的ServiceLoader.load(MyService.class)调用开始分析源代码。load方法代码如下:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 获取当前线程的context classloader
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 调用另一个重载方法,传入classloader
        return ServiceLoader.load(service, cl);
    }
    
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        // 新建一个ServiceLoader
        return new ServiceLoader<>(service, loader);
    }
    

    继续查看ServiceLoader的构造方法:

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // 检查svc不能为null
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 如果没传入classloader,则使用系统的classloader
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        // 获取访问控制器,Java安全机制
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        // 调用reload方法
        reload();
    }
    

    reload方法内容如下所示。其中providers变量为一个map,保存了服务具体实现类的名称(包含包名)和实现类这个对象本身。这是ServiceLoader的一种缓冲机制,同时ServiceLoader使用了懒加载,实现类只有第一次迭代ServiceLoader的时候才会创建出,加入到providers缓存中再返回。以后无论再迭代多少次,都是从providers缓存中获取。

    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    

    LazyIterator为懒加载迭代器。这里先不介绍,到调用到它的时候再分析。

    ServiceLoader创建完毕后我们开始迭代它了。创建迭代器的逻辑在ServiceLoaderiterator方法:

    public Iterator<S> iterator() {
        return new Iterator<S>() {
    
            // 创建一个providers缓存的key值迭代器
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
    
            public boolean hasNext() {
                // 先迭代已缓存的实现类,如果没有,则迭代懒加载迭代器
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
    
            public S next() {
                // 同样,如果providers保存的有实现类,优先返回
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 否则去懒加载迭代器查找下一个实现类
                return lookupIterator.next();
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        };
    }
    

    到这里我们需要继续分析LazyIteratornext方法了。

    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    

    先忽略AccessController这块儿,next方法调用的是nextService方法,继续查看。

    private S nextService() {
        // 如果没有下一个service,抛出异常
        // 这个方法很重要,稍后分析
        if (!hasNextService())
            throw new NoSuchElementException();
        // nextName变量保存的是下一个要迭代的服务实现类全名
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // 使用反射创建出这个实现类
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        // 如果实现类c不是Service类型,报错退出
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            // 将c转换为Service类型
            S p = service.cast(c.newInstance());
            // 将c放入providers缓存
            providers.put(cn, p);
            // 返回转换为Service类型的实现类
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    

    hasNextService方法如下所示。

    private boolean hasNextService() {
        // 如果有下一个要迭代的实现类名,返回true
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                // PREFIX为META-INF/services/,正是配置文件的位置
                // 这里在拼接配置文件路径
                String fullName = PREFIX + service.getName();
                // 然后加载这个配置文件
                // 获取到配置文件绝对路径
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        // 一行一行的解析配置文件
        // pending是获取解析过的配置文件内容的迭代器
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                // 如果pending和configs都没有下一个元素,返回false
                return false;
            }
            // 解析配置文件,返回迭代器
            pending = parse(service, configs.nextElement());
        }
        // 将配置文件中待解析的一行配置放入nextName变量中。
        nextName = pending.next();
        return true;
    }
    

    到这里ServiceLoader的实现原理我们已经清楚了。使用的仍然是Java 反射方式,从约定好位置的配置文件中读取各个实现类的名称,通过Class.forname方式创建出实例并返回。

    相关文章

      网友评论

        本文标题:Java SPI 简介

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