美文网首页JavaAndroid开发经验谈Android开发
Java | 带你理解 ServiceLoader 的原理与设计

Java | 带你理解 ServiceLoader 的原理与设计

作者: 彭旭锐 | 来源:发表于2020-06-16 17:39 被阅读0次

    前言

    • ServiceLoaderJava提供的一套SPI(Service Provider Interface,常译:服务发现)框架,用于实现服务提供方与服务使用方解耦
    • 在这篇文章里,我将带你理解ServiceLoader的原理与设计思想,希望能帮上忙

    目录


    1. SPI 简介

    • 定义
      一个服务的注册与发现机制
    • 作用
      通过解耦服务提供者与服务使用者,帮助实现模块化、组件化

    2. ServiceLoader 使用步骤

    我们直接使用JDBC的例子,帮助各位建立起对ServiceLoader的基本了解,具体如下:

    我们都知道JDBC编程有五大基本步骤:

      1. 执行数据库驱动类加载(非必须):Class.forName("com.mysql.jdbc.driver")
      1. 连接数据库:DriverManager.getConnection(url, user, password)
      1. 创建SQL语句:Connection#.creatstatement();
      1. 执行SQL语句并处理结果集:Statement#executeQuery()
      1. 释放资源:ResultSet#close()Statement#close()Connection#close()

    操作数据库需要使用厂商提供的数据库驱动程序,直接使用厂商的驱动耦合太强了,更推荐的方法是使用DriveManager管理类:

    步骤1:定义服务接口

    JDBC抽象出一个服务接口,数据库驱动实现类统一实现这个接口:

    public interface Driver {
        // 创建数据库连接
        Connection connect(String url, java.util.Properties info)
            throws SQLException;
    
        // 省略其他方法...
    }
    

    步骤2:实现服务接口

    服务提供者(数据库厂商)提供一个或多个实现这个服务的类(驱动实现类),具体如下:

    • mysqlcom.mysql.cj.jdbc.Driver.java
    
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        static {
            try {
                // 注册驱动
                java.sql.DriverManager.registerDriver(new Driver());
            } catch (SQLException E) {
                throw new RuntimeException("Can't register driver!");
            }
        }
        // 省略...
    }
    
    • oracleoracle.jdbc.driver.OracleDriver.java
    public class OracleDriver implements Driver {
        private static OracleDriver defaultDriver = null;
        static {
            try {
                if (defaultDriver == null) {
                    //1. 单例
                    defaultDriver = new OracleDriver();
                    // 注册驱动
                    DriverManager.registerDriver(defaultDriver);
                }
            } catch (RuntimeException localRuntimeException) {
                ;
            } catch (SQLException localSQLException) {
                ;
            }
        }
    
        // 省略...
    }
    

    步骤3:注册实现类到配置文件

    java的同级目录中新建目录resources/META-INF/services,新建一个配置文件java.sql.Driver(文件名为服务接口的全限定名),文件中每一行是实现类的全限定名,例如:

    com.mysql.cj.jdbc.Driver
    

    我们可以解压mysql-connector-java-8.0.19.jar包,找到对应的META-INF文件夹。

    步骤4:加载服务

    // DriverManager.java
    static {
        loadInitialDrivers();
    }
    
    private static void loadInitialDrivers() {
        // 省略次要代码...
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 使用ServiceLoader遍历实现类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                // 获得迭代器
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 迭代
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                        // 疑问:为什么没有任何处理?
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
            }
        });
        // 省略次要代码...
    }
    

    可以看到,DriverManager的静态代码块调用loadInitialDrivers (),方法内部通过ServiceLoader提供的迭代器Iterator<Driver>遍历了所有驱动实现类,但是为什么在迭代里没有任何操作呢?

    while(driversIterator.hasNext()) {
        driversIterator.next();
        // 疑问:为什么没有任何处理?
    }
    

    在下一节,我们深入ServiceLoader的源码来解答这个问题。


    3. ServiceLoader 源码解析

    # 提示 #

    ServiceLoader中有一些源码使用了安全检测,如AccessController.doPrivileged(),在以下代码摘要中省略

    • 工厂方法
      ServiceLoader提供了三个静态泛型工厂方法,内部最终将调用ServiceLoader.load(Class,ClassLoader),具体如下:
    // 1.
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        // 使用双亲委派模型中最顶层的ClassLoader
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }
    
    // 2.
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 使用线程上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
    // 3.
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }
    

    可以看到,三个方法仅在传入的ClassLoader参数有区别,若还不了解ClassLoader,请务必阅读[《Java | 带你理解 ClassLoader 的原理与设计思想》](Editting...)

    • 构造方法
    private final Class<S> service;
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
    private ServiceLoader(Class<S> svc, ClassLoader cl) { 
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        reload();
    }
    
    public void reload() {
        // 清空 providers
        providers.clear();
        // 实例化 LazyIterator
        lookupIterator = new LazyIterator(service, loader);
    }
    

    可以看到,ServiceLoader的构造器中创建了LazyIterator迭代器的实例,这是一个“懒加载”的迭代器。那么这个迭代器在哪里使用的呢?继续往下看~

    • 外部迭代器
    private LazyIterator lookupIterator;
    
    // 返回一个新的迭代器,包装了providers和lookupIterator
    public Iterator<S> iterator() {
        return new Iterator<S>() {
    
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
    
            public boolean hasNext() {
                // 优先从knownProviders取
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
    
            public S next() {
                // 优先从knownProviders取
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        };
    }
    

    可以看到,ServiceLoader里有一个泛型方法Iterator<S> iterator(),它包装了providers集合迭代器和lookupIterator两个迭代器,迭代过程中优先从providers获取元素。

    为什么要优先从providers集合中取元素呢?阅读源码发现,LazyIterator#next()会将每轮迭代中取到的元素putproviders集合中,providers其实是LazyIterator的内存缓存。

    • 内部迭代器
    # 提示 #

    以下代码摘要中省略了源码中的try-catch

    // ServiceLoader.java
    
    private static final String PREFIX = "META-INF/services/";
    
    private class LazyIteratorimplements Iterator<S> {
    
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
    
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
        
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                // configs 未初始化才执行
                // 配置文件:META-INF/services/服务接口的全限定名
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            }
            
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 分析点1:解析配置文件资源
                pending = parse(service, configs.nextElement());
            }
            // nextName:下一个实现类的全限定名
            nextName = pending.next();
            return true;
        }
    
        private S nextService() {
            if (!hasNextService()) throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            // 1. 使用类加载器loader加载
            Class<?> c = Class.forName(cn, false, loader);
            if (!service.isAssignableFrom(c)) {
                ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
                fail(service, "Provider " + cn  + " not a subtype", cce);
            }
            // 2. 根据Class实例化服务实现类
            S p = service.cast(c.newInstance());
            // 3. 服务实现类缓存到 providers
            providers.put(cn, p);
            return p;
        }
    
        public boolean hasNext() {
            return hasNextService();
        }
    
        public S next() {
            return nextService();
        }
    
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    // 分析点1:解析配置文件资源,实现类的全限定名列表迭代器
    private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
        // 使用 UTF-8 编码输入配置文件资源
        InputStream in = u.openStream();
        BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        ArrayList<String> names = new ArrayList<>();
        int lc = 1;
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        return names.iterator();
    }
    

    4. ServiceLoader 要点

    理解ServiceLoader源码之后,我们总结要点如下:

    • 约束

      • 服务实现类必须实现服务接口S,参见源码:if (!service.isAssignableFrom(c))
      • 服务实现类需包含无参的构造器,ServiceLoader将通过该无参的构造器来创建服务实现者的实例,参见源码:S p = service.cast(c.newInstance());
      • 配置文件需要使用UTF-8编码,参见源码:new BufferedReader(new InputStreamReader(in, "utf-8"));
    • 懒加载
      ServiceLoader使用“懒加载”的方式创建服务实现类实例,只有在迭代器推进的时候才会创建实例,参见源码:nextService()

    • 内存缓存
      ServiceLoader使用LinkedHashMap缓存创建的服务实现类实例,LinkedHashMap在二次迭代时会按照Map#put执行顺序遍历

    • 服务实现的选择
      当存在多个提供者时,服务消费者模块不一定要全部使用,而是需要根据某些特性筛选一种最佳实现。ServiceLoader的机制只能在遍历整个迭代器的过程中,从发现的实现类中决策出一个最佳实现,例如使用Charset.forName(String)获得Charset实现类:

    // 服务接口
    public abstract class CharsetProvider {
        public abstract Charset charsetForName(String charsetName);
        // 省略其他方法...
    }
    
    // Charset.java
    public static Charset forName(String charsetName) {
        // 以下只摘要与ServiceLoader有关的逻辑
        ServiceLoader<CharsetProvider> sl = ServiceLoader.load(CharsetProvider.class, cl);
        Iterator<CharsetProvider> i = sl.iterator();
        for (Iterator<CharsetProvider> i = providers(); i.hasNext();) {
            CharsetProvider cp = i.next();
            // 满足匹配条件,return
            Charset cs = cp.charsetForName(charsetName);
            if (cs != null)
                return cs;
        }
    }
    
    
    • ServiceLoader没有提供服务的注销机制
      服务实现类实例被创建后,它的垃圾回收的行为与Java中的其他对象一样,只有这个对象没有到GC Root的强引用,才能作为垃圾回收。

    5. 问题回归

    现在我们回到阅读DriverManager源码提出的疑问:

    while(driversIterator.hasNext()) {
        driversIterator.next();
        // 疑问:为什么没有任何处理?
    }
    

    为什么next()操作既不取得服务实现类对象,后续也没有任何处理呢?我们再回去看下LazyIterator#next()的源码:

    // ServiceLoader.java
    
    // next() 直接调用 nextService()
    private S nextService() {
        if (!hasNextService()) throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        // 1. 使用类加载器loader加载
        Class<?> c = Class.forName(cn, false, loader);
        if (!service.isAssignableFrom(c)) {
            ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
            fail(service, "Provider " + cn  + " not a subtype", cce);
        }
        // 2. 根据Class实例化服务实现类
        S p = service.cast(c.newInstance());
        // 3. 服务实现类缓存到 providers
        providers.put(cn, p);
        return p;
    }
    
      1. 使用类加载器loader加载:Class<?> c = Class.forName(cn, false, loader);
        这里传参使用false,类加载器将执行加载 -> 链接,不会执行初始化
      1. 根据 Class 实例化服务实现类
        由于创建类实例前一定会保证类加载完成,因此这里类加载器隐式执行了初始化,这就包括了类的静态代码块执行

    回过头看com.mysql.cj.jdbc.Driveroracle.jdbc.driver.OracleDriver源码,我们都发现了类似的静态代码块:

    static {
        try {
            // 注册驱动
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    

    可以看到,它们都调用了DriverManager#registerDriver注册了一个服务实现类实例,保存在CopyOnWriteArrayList中,后续获取数据库连接时是从这个列表中获取数据库驱动。现在,你理解了吗?


    6. 总结

      1. ServiceLoader基于 SPI 思想,可以实现服务提供方与服务使用方解耦,是模块化组件化的一种实现方式
      1. ServiceLoader是一个相对简易的框架,往往只在Java源码中使用,为了满足复杂业务的需要,一般会使用提供SPI功能的第三方框架,例如后台的Dubbo、客户端的ARouterWMRouter

    在后面的文章中,我将与你探讨ARouterWMRouter的源码实现,欢迎关注彭旭锐的博客。


    参考资料


    推荐阅读

    感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

    相关文章

      网友评论

        本文标题:Java | 带你理解 ServiceLoader 的原理与设计

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