Java SPI 机制原理分析

JDK SPI 机制原理分析

最近开始看 Dubbo 源码, 而 Dubbo 的一大优秀设计就是 Dubbo SPI 机制, 而 Dubbo 的 SPI 是对 JDK 的 SPI 的增强, 所以先对 JDK SPI 机制 准备做一个分析.同时也建议大家多读优秀框架的源码.

1 SPI 简介

1.1 什么是 SPI

SPI全称 Service Provider Interface ,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。

1.2 SPI和API的使用场景

  • API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

  • SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

2 JDK SPI 快速上手

先从 HelloWorld级 的代码入手, 让大家体验一下 SPI 机制.

2.1 定义接口 Action (名字随意, 我这里就叫 Action 了)


package com.inetsoft.spi;

public interface Action {

   String doAction(String name) throws Exception;

}

2.2 为接口添加两个实现--- UploadActionDownloadAction

  • UploadAction

package com.inetsoft.spi.impl;

import com.inetsoft.spi.Action;

public class UploadAction implements Action {

   public UploadAction() {
      System.out.println("Init UploadAction...");
   }

   @Override
   public String doAction(String name) {
      return "Upload " + name;
   }
}
  • DownloadAction

package com.inetsoft.spi.impl;

import com.inetsoft.spi.Action;

public class DownloadAction implements Action {
   @Override
   public String doAction(String name) throws Exception {
      if(name == null) {
         throw new Exception("Unsupport action for " + name);
      }

      return "Download " + name;
   }
}

2.3 添加 SPI 描述文件

在resources目录下新建 META-INF/services 目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名

file

file

2.4 通过 ServiceLoader 加载实现类并调用接口方法


import com.inetsoft.spi.Action;
import java.util.Iterator;
import java.util.ServiceLoader;

public class App {
    public static void main(String[] args) throws Exception {

       // 创建 ServiceLoader 对象, ServiceLoader 实现了 Iterable, 因此可迭代遍历
       ServiceLoader<Action> actions = ServiceLoader.load(Action.class);

       // 获取 ServiceLoader 的迭代器
       Iterator<Action> iterator = actions.iterator();

       while(iterator.hasNext()) {
          // next 方法中会根据配置文件对接口实现类进行实例化(反射: newInstance)
          Action action = iterator.next();
          // 调用具体方法
          System.out.println(action.doAction("JDK SPI"));
       }
    }
}

file

到这里一个简单的 JDK SPI 的 HelloWorld 就完成了, 可以看到其中最为核心的就是通过 ServiceLoaderload 方法. 下面原理分析我们就一起从这里入手分析一下 JDK SPI 机制的原理

3 JDK SPI 机制的原理

精髓都在代码中进行了注释, 而且代码顺序帅帅已经调整好, 也将不相关的代码移除了, 因此大家可以自上而下将下面代码读一遍就可以了.

上面其实也已经说过了, JDK SPI 机制的原理我们可以从 ServiceLoaderload 方法入手.


    // 配置文件所在目录
    private static final String PREFIX = "META-INF/services/";

     // 创建ServiceLoader时获得的访问控制上下文
    private final AccessControlContext acc;

     // 缓存的提供者,按实例化顺序排序
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 惰性查找迭代器, 用于扫描 SPI 配置文件(包括所有引用的 jar 中的) ----> 只有调用迭代器的 Next 方法时才会去创建实现类对象(反射)
    private LazyIterator lookupIterator;

    // ...

    // SPI 入口方法
    public static <S> ServiceLoader<S> load(Class<S> service) {
            // 上下文类加载器 ----> 为什么获取当前线程的 ContextClassLoader ? -----> 这牵扯到 JVM 的类加载机制, 后面会单独拿出来说, 核心就在 JVM 类加载机制的 <<双亲委派模型>>
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
                // 调用重载方法
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
             // 创建 ServiceLoader 对象, 核心在构造
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
                // ClassLoader 如果为空就用系统类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
                // 获取访问控制上下文对象
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
                // reload 实例
        reload();
    }

    public void reload() {
              // 先清空缓存的, 第一次访问本身就为空
        providers.clear();
                // 创建惰性加载迭代器
        lookupIterator = new LazyIterator(service, loader);
                // 看到这里, 实际上 ServiceLoader.load 方法就已经执行完了, 所以可以发现实现类对象还没有在这个过程中创建
    }

        // 内部类 ---- 惰性加载迭代器
    private class LazyIterator
        implements Iterator<S>
    {
                // 接口对象
        Class<S> service;
                // 类加载器
        ClassLoader loader;

                // 将配置文件信息封装为 URL 对象的迭代器, 是一个 CompoundEnumeration 对象
        Enumeration<URL> configs = null;
                // 循环解析配置文件的迭代器, 辅助遍历 configs/CompoundEnumeration
        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;
            }
                        // 第一次访问 configs 为空
            if (configs == null) {
                try {
                                        // 获取配置文件的路径: META-INF/services/com.inetsoft.spi.Action
                    String fullName = PREFIX + service.getName();

                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                                              // 类加载不为空, 通过 ClassLoader 将配置文件信息封装为 CompoundEnumeration<URL> 枚举
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
                        // 通过 pending 对象遍历 configs
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                                // 将配置文件信息解析到 pending ---> 这里会通过 utf-8 的格式读 META-INF/services/com.inetsoft.spi.Action
                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());
                                // 缓存实例对象 ---> reload 会清空
                providers.put(cn, p);
                                // 返回实例对象
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

                // ...
    }
  • URL 对象

file

  • 基本流程图

file

4. Java SPI 的弊端

从上面的java spi的原理中可以了解到,java的spi机制有着如下的弊端:

  • 只能遍历所有的实现,并全部实例化。
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
  • 扩展如果依赖其他的扩展,做不到自动注入和装配。
  • 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持。

5. 预览 Dubbo SPI 机制

正如文章开头所说, Dubbo SPI 从 JDK 标准的 SPI 扩展点发现机制加强而来, 因此我们后边会单独拿出来一篇文章来说, 这里先进行一个简单的说明. 下面拿出 Dubbo 官网对于 Dubbo SPI 的介绍

file

感兴趣的同学可以参阅 Dubbo 官网 SPI 机制( https://dubbo.apache.org/zh/docs/v2.7/dev/spi/ ), 后面我们会单独来说 Dubbo SPI 机制.

本文由博客一文多发平台 OpenWrite 发布!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值