暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JAVA SPI源码

爱做菜的程序猿 2021-11-17
318

JAVA SPI源码

上一篇:Java SPI

  • 由上面的案例我们知道JAVA SPI的入口是ServiceLoader.load(),我们直接ctrl+左键进入源码查看:

    public static <S> ServiceLoader<S> load(Class<S> service) {
       //获取当前线程的classLoader,大部分情况类似getClassLoader
       ClassLoader cl = Thread.currentThread().getContextClassLoader();
       return ServiceLoader.load(service, cl);
  }
  • 跟进ServiceLoader.load方法:

    //为给定的服务类型和类加载器创建一个新的服务加载器
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
   return new ServiceLoader<>(service, loader);
}
  • 再跟进创建出来的服务加载器ServiceLoader

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
       //如果svc的class对象为空,则报空指针异常,报错信息为Service interface cannot be null
       service = Objects.requireNonNull(svc, "Service interface cannot be null");
       //判断类加载器cl是否为空,为空则使用系统的类加载器SystemClassLoader
       loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
       acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
       reload();
  }
  • 跟进最后的方法reload方法

    public void reload() {
       //清除此加载程序的提供程序缓存,以便重新加载所有提供程序。
       providers.clear();
       //创建LazyIterator,是Iterator的一个实现类,也是迭代器
       lookupIterator = new LazyIterator(service, loader);
  }
  • LazyIterator,是Iterator的一个实现类,也是迭代器,下面我们看一下它做了什么:

    • acc:创建 ServiceLoader 时获取的访问控制上下文

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);
  }
}

可以看出不论acc这个AccessControlContext快照做了什么,都会去执行hasNextService。

  • (最重要!)跟进查看hasNextService方法:这里的hasNextService就是会去获取每个方法的实现类,获取不到则返回false

        private static final String PREFIX = "META-INF/services/"; //约定的目录
private boolean hasNextService() {
           //如果下一个不为空则直接返回真
           if (nextName != null) {
               return true;
          }
           if (configs == null) {
               try {
                   //得到文件位置,即META-INF/services/cn.zsp.spi.Car
                   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()) {
                   //校验配置文件是否还有其他元素,没有就返回false,防止无限循环
                   return false;
              }
               pending = parse(service, configs.nextElement());//解析内容成配置文件
          }
           nextName = pending.next();
           return true;
      }

hasNextService这个方法主要执行了:配置文件路径(fullName)--->根据路径读取配置文件(configs)--->遍历文件内容--->将文件内容解析成配置文件。

这里有一块较难的parse方法,最后会返回迭代器,我深入讲一下,不想看的直接看下一个黑点标记即可:

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();
  }

前面就是读取流,没什么讲的,有意思的是parseLine这个方法,这是SPI自己定义的:


   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('#');
       //注意,此时ln成为了主要语句,类似于:
       //server.port=80#端口号是80
       //被截断成为server.port=80
       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);//获取开头字符编码,比如a就是97
           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);//每次都去获取遍历字符的编码
               //如果cp是字符,且它不是“.”这个字符,例如 spring.DataSource.url=xxxxx
               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;
  }

总结:这两个方法的目的就是,使用流遍历配置文件,将每一行读取出来放入方法集中,最后返回方法集的迭代器。

  • 到了方法的最后使用了next,使用完毕后才走了true判断有配置文件,进入看一下next方法,这个next方法是SPI自定义的:

        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);
          }
      }
  • 可以发现next方法到最后都会调用nextService方法,进入查看一下:

 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)) {
               //如果service是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
      }

最终,我们终于看到了providers.put(cn, p)这一步,它将实现类名和实现的类放入了缓存。至此,JAVA SPI的所有源码我们看完了。比起大多数源码来说,相对简单。

总结:

  • 约定一个目录:META-INF/services/

  • 根据接口名找到对应的文件

  • 文件解析得到实现类的全限定名

  • 循环加载实现类和创建实例

整体流程图


文章转载自爱做菜的程序猿,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论