jannal(无名小宝)

没有失败,只有缓慢的成功

导航

Dubbo之SPI机制

Dubbo版本

  1. Dubbo版本2.6.7

JDK SPI

介绍

  1. SPI(Service provider interface的缩写)是JDK内置的一种服务提供发现机制,符合对修改封闭,对扩展开放的原则
  2. 作用: 基于某种约定为接口查找相应的实现类(一个或者多个),主要是针对不同的服务提供厂商,对不同场景的提供不同的解决方案制定的一套标准,比如JDBC驱动
  3. jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

使用方式

  1. 当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

  2. 创建一个META-INF/services/目录,并放到ClassPath目录下

  3. 关于META-INF/services的文件格式:

    • 文件名字必须是SPI接口类的全名
    • 文件的内容是SPI服务具体实现类的全限定名,如果有多个则使用分隔符分隔
    • 文件采用UTF-8编码,用#来表示注释
    • 关于SPI接口以及实现类:实现类必须要有一个无参的构造方法,ServiceLoader支持懒加载方式,只有当需要的时候,才去加载服务的实现类。
  4. 调用:

    ServiceLoader.load(xxxx.class);
    
  5. jdbc4.0以前,开发人员还需要基于Class.forName("xxx")的方式来装载驱动,jdbc4也基于spi的机制来发现驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者。比如mysql-connector-java-8.0.19.jar

    // 查看文件META-INF/services/java.sql.Driver
    com.mysql.cj.jdbc.Driver
    
    //java.sql.DriverManager有一个静态代码块
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    private static void loadInitialDrivers() {
      ...省略...
      ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
      Iterator<Driver> driversIterator = loadedDrivers.iterator();  
      ...省略...
    }
    

案例

  1. 定义接口和实现类

    1. 定义接口
    public interface BusinessInterface {
    	void dosomething(String username);
    }
    2. 实现类
    public class RealBusinessImpl implements BusinessInterface {
        @Override
        public void dosomething(String username) {
            System.out.println("正在为用户:" + username + ",进行真实的业务处理。。。");
        }
    }
    
  2. 配置:创建META-INF/services目录,在此目录下创建以接口全限定名为名称的文件cn.jannal.spi.BusinessInterface在此文件中写具体实现的全限定名cn.jannal.spi.impl.RealBusinessImpl

  3. 调用

    public class Main {
        public static void main(String[] args) throws RuntimeException {
          	//普通创建对象方式
            nospi();
          	//spi方式加载
            spi();
        }
    
        private static void nospi() {
            BusinessInterface realBusiness = new RealBusinessImpl();
            realBusiness.dosomething("普通方式");
        }
    
        private static void spi() {
            ServiceLoader<BusinessInterface> businessInterface = ServiceLoader.load(BusinessInterface.class);
            //获取所有的实现,具体调用哪个由自己决定
            Iterator<BusinessInterface> iinterface = businessInterface.iterator();
            if (iinterface.hasNext()) {
                BusinessInterface interfaceItem = iinterface.next();
                interfaceItem.dosomething("spi方式");
            }
        }
    }
    
  4. 运行结果

    正在为用户:普通方式,进行真实的业务处理。。。
    正在为用户:spi方式,进行真实的业务处理。。。
    

源码分析

  1. ServiceLoader核心源码分析

    public final class ServiceLoader<S>
        implements Iterable<S>
    {
       //查找配置文件的目录,这里可以看到,使用默认的方式加载,这个目录是写死的,ServiceLoader在load时提供Classloader,则可以从其他的目录读取。
        private static final String PREFIX = "META-INF/services/";
    
        // 要被加载的服务类或接口
        private final Class<S> service;
    
        // 这个ClassLoader用来定位,加载,实例化服务提供者
        private final ClassLoader loader;
    
        //访问控制
        private final AccessControlContext acc;
    
        //缓存已经被实例化的服务提供者,按照实例化的顺序存储 <接口全名称, 实现类实例>
        private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    
        // 懒加载查找迭代器(真实的迭代器),不对外暴露,外层迭代器委托此迭代器进行操作
        private LazyIterator lookupIterator;
    
        /**
         * 重新加载,就相当于重新创建ServiceLoader了,用于新的服务提供者安装到正在运行的Java虚拟机中的情况。
         */
        public void reload() {
        	//清空缓存中所有的服务提供者(实现类)
            providers.clear();
            //创建一个新的迭代器
            lookupIterator = new LazyIterator(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();
        }
    
        private static void fail(Class<?> service, String msg, Throwable cause)
            throws ServiceConfigurationError
        {
            throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                                cause);
        }
    
        private static void fail(Class<?> service, String msg)
            throws ServiceConfigurationError
        {
            throw new ServiceConfigurationError(service.getName() + ": " + msg);
        }
    
        private static void fail(Class<?> service, URL u, int line, String msg)
            throws ServiceConfigurationError
        {
            fail(service, u + ":" + line + ": " + msg);
        }
    
        /*
        * 解析配置文件的一行,
        */
        private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                              List<String> names)
            throws IOException, ServiceConfigurationError
        {
            String ln = r.readLine();
            if (ln == null) {
                return -1;
            }
            int ci = ln.indexOf('#');
            if (ci >= 0) ln = ln.substring(0, ci);
            ln = ln.trim();
            int n = ln.length();
            if (n != 0) {
                if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                    fail(service, u, lc, "Illegal configuration-file syntax");
                int cp = ln.codePointAt(0);
                if (!Character.isJavaIdentifierStart(cp))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
                for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                    cp = ln.codePointAt(i);
                    if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                        fail(service, u, lc, "Illegal provider-class name: " + ln);
                }
                if (!providers.containsKey(ln) && !names.contains(ln))
                    names.add(ln);
            }
            return lc + 1;
        }
    
        
        private Iterator<String> parse(Class<?> service, URL u)
            throws ServiceConfigurationError
        {
            InputStream in = null;
            BufferedReader r = null;
            ArrayList<String> names = new ArrayList<>();
            try {
                in = u.openStream();
                r = new BufferedReader(new InputStreamReader(in, "utf-8"));
                int lc = 1;
                while ((lc = parseLine(service, u, r, lc, names)) >= 0);
            } catch (IOException x) {
                fail(service, "Error reading configuration file", x);
            } finally {
                try {
                    if (r != null) r.close();
                    if (in != null) in.close();
                } catch (IOException y) {
                    fail(service, "Error closing configuration file", y);
                }
            }
            return names.iterator();
        }
    
        // Private inner class implementing fully-lazy provider lookup
        //
        private class LazyIterator
            implements Iterator<S>
        {
    
            Class<S> service;
            ClassLoader loader;
            //实现类的URL路径
            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();
            }
    
        }
    
        /**
        * 以懒加载的方式加载可用的服务提供者
        * 懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
        **/
        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();
                }
    
                public S next() {
                    if (knownProviders.hasNext())
                        return knownProviders.next().getValue();
                    return lookupIterator.next();
                }
    
                public void remove() {
                    throw new UnsupportedOperationException();
                }
    
            };
        }
    
       public static <S> ServiceLoader<S> load(Class<S> service,
                                                ClassLoader loader){
            return new ServiceLoader<>(service, loader);
        }
    
       public static <S> ServiceLoader<S> load(Class<S> service) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    
    
        public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            ClassLoader prev = null;
            while (cl != null) {
                prev = cl;
                cl = cl.getParent();
            }
            return ServiceLoader.load(service, prev);
        }
    
        
        public String toString() {
            return "java.util.ServiceLoader[" + service.getName() + "]";
        }
    
    }
    
  2. 执行顺序

     ServiceLoader.load(Xxxx.class)   获取当前类的类加载器
    	--> ServiceLoader(Class<S> svc, ClassLoader cl) 
    		--> reload() : 清空服务提供者,并且创建一个懒加载迭代器
    		
     Iterator<XxxxInterface> iinterface = XxxxInterface.iterator()
     	--> 返回一个迭代器(委托LazyIterator提供逻辑)
     	
     		
     iinterface.hasNext()	
     	-->  lookupIterator.hasNext()
     		--> hasNextService()
     			--> classLoader.getResources(fullName) //从META-INF/services/加载文件
     				--> parse(Class<?> service, URL u)  // 解析文件
     					--> new BufferedReader(new InputStreamReader(in, "utf-8")); //字符编码必须是utf-8
     						--> parseLine()   //解析一行
    
    iinterface.next()
    	--> nextService()
        //这里可以看到,服务实现类必须提供无参数的构造方法,否则就无法实例化
    		--> S p = service.cast(c.newInstance()) 			
    
  3. 从上面可以看出,在调用load方法时并没有加载配置文件和实例化服务实现类,而是在调用迭代器的hasNext()时才去加载文件并解析。所有的配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用reload方法(第一次加载其实也是调用了reload)

缺点

  1. JDK标准的SPI会一次性实例化扩展点所有实现(遍历所有实现并实例化),如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  2. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
  3. 从源码来看,不是线程安全的

Dubbo SPI

  1. Dubbo 改进了 JDK 标准的 SPI 的以下问题

    • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,即使没用也会加载,很浪费资源。Dubbo SPI只是加载配置文件中的类,并分成不同的种类缓存在内存中,不会立即全部初始化

    • 如果扩展点加载失败,连扩展点的名称都拿不到,异常信息可能丢失,追踪问题比较困难。Dubbo SPI在扩展加载失败的时候会先抛出真实异常并打印日志。扩展点在被加载时,及时有部分扩展加载失败也不会影响其他扩展点和整个框架的使用

    • 增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点

    • 可以方便的获取某一个想要的扩展实现,java的SPI机制就没有提供这样的功能

    • 对于扩展实现IOC依赖注入功能:举例来说:接口A,实现者A1、A2。接口B,实现者B1、B2。现在实现者A1含有setB()方法,会自动注入一个接口B的实现者,此时注入B1还是B2呢?都不是,而是注入一个动态生成的接口B的实现者B$Adpative,该实现者能够根据参数的不同,自动引用B1或者B2来完成相应的功能

    • 兼容JDK SPI

  2. Dubbo默认扫描

    • META-INF/dubbo/internal/
    • META-INF/service/
    • META-INF/dubbo/
  3. 配置规范

    • 配置文件名称是接口全限定名
    • 配置目录 META-INF/dubbo/internal/ META-INF/service/ META-INF/dubbo/
    • 文件内容格式,key=value,多个使用换行分隔

使用方式

  1. 添加依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo-common</artifactId>
        <version>${project.version}</version>
    </dependency>
    
  2. 定义接口和实现类,并为接口添加@SPI注解,设置默认实现为real

    1. 定义接口
    package cn.jannal.spi;  
    @SPI("real")
    public interface BusinessInterface {
     	void dosomething(String username);
    }
    
    2. 实现类
    package cn.jannal.spi.impl;  
    public class RealBusinessImpl implements BusinessInterface {
        @Override
        public void dosomething(String username) {
            System.out.println("正在为用户:" + username + ",进行真实的业务处理。。。");
        }
    }
    
  3. 步骤

    • 在目录META-INF/dubbo/internal下建立配置文件,名称为接口全限定名称cn.jannal.spi.BusinessInterface
    • 文件内容为real=cn.jannal.spi.impl.RealBusinessImpl,即配置名=扩展实现类全限定名,多个实现类用换行符分隔
    • 使用ExtensionLoader(相当于JDK中的ServiceLoader
  4. 调用

    public class Main {
        public static void main(String[] args) {
            ExtensionLoader<BusinessInterface> extensionLoader = ExtensionLoader.getExtensionLoader(BusinessInterface.class);
            BusinessInterface businessInterface = extensionLoader.getDefaultExtension();
            //正在为用户:jannal,进行真实的业务处理。。。
            businessInterface.dosomething("jannal");
    
            //通过配置名称获取实现
            final BusinessInterface real = extensionLoader.getExtension("real");
            //正在为用户:jack,进行真实的业务处理。。。
            real.dosomething("jack");
        }
    }
    

posted on 2021-12-25 19:42  jannal  阅读(75)  评论(0)    收藏  举报