• JAVA SPI机制分析


    简介

    SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

    SPI具体约定

    Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

    简单示例

    首先我们提供一个接口类 IOperation 以及它的两个实现类 PlusOperationImplDivisionOperationImpl,都在 com.qhong.spi 这个包路径下。

    package com.qhong.spi;
    
    /**
     * @author qhong
     * @date 2020/3/22 0:16
     **/
    public interface IOperation {
    
    	int operation(int numberA, int numberB);
    }
    
    
    package com.qhong.spi;
    
    /**
     * @author qhong
     * @date 2020/3/22 0:17
     **/
    public class PlusOperationImpl implements IOperation {
    
    	public PlusOperationImpl() {
    		System.out.println("plusOperation construct");
    	}
    
    	public int operation(int numberA, int numberB) {
    		System.out.println("Operation: plus");
    
    		return numberA + numberB;
    	}
    }
    
    package com.qhong.spi;
    
    /**
     * @author qhong
     * @date 2020/3/22 0:17
     **/
    public class DivisionOperationImpl implements IOperation {
    
    	public DivisionOperationImpl() {
    		System.out.println("division construct");
    	}
    
    	@Override
    	public int operation(int numberA, int numberB) {
    		System.out.println("Operation: division");
    		return numberA / numberB;
    	}
    }
    

    新建spi配置文件

    在resources文件夹下新建META-INF文件夹,并创建/services/com.qhong.spi.IOperation文件

    测试

    public class testSPI {
    	public static void main(String[] args) {
    		ServiceLoader<IOperation> operations = ServiceLoader.load(IOperation.class);
    
    		int numberA = 6;
    		int numberB = 3;
    		System.out.println("NumberA: " + numberA + ", NumberB: " + numberB);
    		Iterator<IOperation> iterator = operations.iterator();
    		while (iterator.hasNext()) {
    			IOperation operation = iterator.next();
    			System.out.println(operation.operation(numberA, numberB));
    		}
    	}
    }
    
    

    Output:

    NumberA: 6, NumberB: 3
    division construct
    Operation: division
    2
    plusOperation construct
    Operation: plus
    9
    

    SPI 实现原理

    应用程序调用 ServiceLoader.load 方法

    ServiceLoader.load 方法内先创建一个新的ServiceLoader,并实例化该类中的成员变数,包括:

    • ClassLoader loader(类载入器)
    • AccessControlContext acc(访问控制器)
    • LinkedHashMap<String, S> providers(用于缓存载入成功的类)
    • LazyIterator lookupIterator(实现迭代器功能)

    应用程序通过迭代器获取对象实例

    ServiceLoader 先判断成员变量 providers 对象中否有缓存实例对象,如果有缓存,直接返回。

    如果没有缓存,执行类的装载:读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称,通过反射方法 Class.forName() 载入类对象,并用 instance() 方法将类实例化。把实例化后的类缓存到providers 对象中然后返回实例对象。

    JDBC为例

    java.sql.DriverManager

        private static void loadInitialDrivers() {
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
            // If the driver is packaged as a Service Provider, load it.
            // Get all the drivers through the classloader
            // exposed as a java.sql.Driver.class service.
            // ServiceLoader.load() replaces the sun.misc.Providers()
    
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
    
                    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;
                }
            });
    
            println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
            if (drivers == null || drivers.equals("")) {
                return;
            }
            String[] driversList = drivers.split(":");
            println("number of Drivers:" + driversList.length);
            for (String aDriver : driversList) {
                try {
                    println("DriverManager.Initialize: loading " + aDriver);
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }
    

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

    java.util.ServiceLoader

        public static <S> ServiceLoader<S> load(Class<S> service) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    

    引用

      public static <S> ServiceLoader<S> load(Class<S> service,
                                                ClassLoader loader)
        {
            return new ServiceLoader<>(service, loader);
        }
    

    引用

      private ServiceLoader(Class<S> svc, ClassLoader cl) {
            service = Objects.requireNonNull(svc, "Service interface cannot be null");
            loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
            acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
            reload();
        }
    

    引用

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

    引用

        private class LazyIterator
            implements 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) {
                    try {
                        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);
                    }
                }
                while ((pending == null) || !pending.hasNext()) {
                    if (!configs.hasMoreElements()) {
                        return false;
                    }
                    pending = parse(service, configs.nextElement());
                }
                nextName = pending.next();
                return true;
            }
    
            private S nextService() {
                if (!hasNextService())
                    throw new NoSuchElementException();
                String cn = nextName;
                nextName = null;
                Class<?> c = null;
                try {
                    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
            }
    
            public boolean hasNext() {
                if (acc == null) {
                    return hasNextService();
                } else {
                    PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                        public Boolean run() { return hasNextService(); }
                    };
                    return AccessController.doPrivileged(action, acc);
                }
            }
    
            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);
                }
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        }
    

    注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法

    因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

    再看下看ServiceLoader.load(Class)的代码

        public static <S> ServiceLoader<S> load(Class<S> service) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    

    ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

    到这儿差不多把SPI机制解释清楚了。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

    总结

    JDK 内置的 SPI 机制本身有它的优点,但由于实现比较简单,也有不少缺点。

    优点

    使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用程序可以根据实际业务情况启用或替换具体组件。

    缺点

    • 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
    • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
    • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
    • 加载不到实现类时抛出并不是真正原因的异常,错误很难定位。

    鉴于 SPI 的诸多缺点,很多系统都是自己实现了一套类加载机制,例如 dubbo。用户也可以自定义classloader+反射机制来加载,实现并不复杂。此外开源的类加载解决方案有 Plugin Framework for Java (PF4J) 等。

    参考:

    Java SPI 机制分析及其优缺点

    真正理解线程上下文类加载器(多案例分析)

  • 相关阅读:
    main函数的实现解析
    srand()和rand()函数的使用
    shell编程总结
    自动创建字符设备,不需mknod
    linux使用i/o内存访问外设
    Flink之state processor api原理
    Flink之state processor api实践
    软件架构被高估,清晰简单的设计被低估
    技术架构的战略和战术
    Flink task之间的数据交换
  • 原文地址:https://www.cnblogs.com/hongdada/p/12543654.html
Copyright © 2020-2023  润新知