• JDK源码解析之Java SPI机制


    1. spi 是什么

    SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

    系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了开闭原则,Java SPI就是为某个接口寻找服务实现的机制,Java Spi的核心思想就是解耦。

    整体机制图如下:

    Java的SPI机制

    Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

    总结起来就是:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

    2. 应用场景

    • 数据库驱动加载接口实现类的加载

      JDBC加载不同类型数据库的驱动

    • 日志门面接口实现类加载

      SLF4J加载不同提供应商的日志实现类

    • Spring

      Servlet容器启动初始化org.springframework.web.SpringServletContainerInitializer

    • Spring Boot

      自动装配过程中,加载META-INF/spring.factories文件,解析properties文件

    • Dubbo

      Dubbo大量使用了SPI技术,里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来

      例如Protocol 协议接口

    3. 使用步骤

    以支付服务为例:

    • 创建一个PayService添加一个pay方法

      package com.imooc.spi;
      
      import java.math.BigDecimal;
      
      public interface PayService {
      
          void pay(BigDecimal price);
      }
      

        

    1. 创建AlipayServiceWechatPayService,实现PayService

      ⚠️SPI的实现类必须携带一个不带参数的构造方法;

      package com.imooc.spi;
      
      import java.math.BigDecimal;
      
      public class AlipayService implements PayService{
      
          public void pay(BigDecimal price) {
              System.out.println("使用支付宝支付");
          }
      }
      
      package com.imooc.spi;
      
      import java.math.BigDecimal;
      
      public class WechatPayService implements PayService{
      
          public void pay(BigDecimal price) {
              System.out.println("使用微信支付");
          }
      }
      
    2. resources目录下创建目录META-INF/services

    3. 在META-INF/services创建com.imooc.spi.PayService文件

    4. 先以AlipayService为例:在com.imooc.spi.PayService添加com.imooc.spi.AlipayService的文件内容

    5. 创建测试类

      package com.imooc.spi;
      
      import com.util.ServiceLoader;
      
      import java.math.BigDecimal;
      
      public class PayTests {
      
          public static void main(String[] args) {
              ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class);
              for (PayService payService : payServices) {
                  payService.pay(new BigDecimal(1));
              }
          }
      }
      
    6. 运行测试类,查看返回结果

    4. 原理分析

    首先,我们先打开ServiceLoader<S> 这个类

    
    
      public final class ServiceLoader<S> implements Iterable<S> {
        // SPI文件路径的前缀
        private static final String PREFIX = "META-INF/services/";
      
        // 需要加载的服务的类或接口
        private Class<S> service;
      
        // 用于定位、加载和实例化提供程序的类加载器
        private ClassLoader loader;
      
        // 创建ServiceLoader时获取的访问控制上下文
        private final AccessControlContext acc;
      
        // 按实例化顺序缓存Provider
        private LinkedHashMap<String, S> providers = new LinkedHashMap();
      
        // 懒加载迭代器 
        private LazyIterator lookupIterator;
      
      	......
    }
    

      

     

    参考具体ServiceLoader具体源码,代码量不多,实现的流程如下:

    1. 应用程序调用ServiceLoader.load方法

      // 1. 获取ClassLoad
      public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
      }
      
      // 2. 调用构造方法
      public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
        return new ServiceLoader<>(service, loader);
      }
      
      // 3. 校验参数和ClassLoad
      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();
      }
      
      //4. 清理缓存容器,实例懒加载迭代器
      public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
      }
      

        

       
    2. 我们简单看一下这个懒加载迭代器

      // 实现完全懒惰的提供程序查找的私有内部类
      private class LazyIterator implements Iterator<S>{
      
        // 需要加载的服务的类或接口
        Class<S> service;
        // 用于定位、加载和实例化提供程序的类加载器
        ClassLoader loader;
        // 枚举类型的资源路径
        Enumeration<URL> configs = null;
        // 迭代器
        Iterator<String> pending = null;
        // 配置文件中下一行className
        String nextName = null;
      
        private LazyIterator(Class<S> service, ClassLoader loader) {
          this.service = service;
          this.loader = loader;
        }
      
        private boolean hasNextService() {
          if (nextName != null) {
            return true;
          }
          // 加载配置PREFIX + service.getName()的文件
          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;
        }
      
        // 获取下一个Service实现
        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
        }
      
        // for循环遍历时
        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();
        }
      
      }
      

        

       
    3. 将给定URL的内容作为提供程序配置文件进行分析。

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

        

       
    4. 按行解析配置文件,并保存names列表中

      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('	') >= 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);
                  }
                  // 判断provider容器中是否包含 不包含则讲classname加入 names列表中
                  if (!providers.containsKey(ln) && !names.contains(ln))
                      names.add(ln);
              }
              return lc + 1;
          }
      

       

    5. 总结

    优点:使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

    缺点:线程不安全,虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

  • 相关阅读:
    Objective C
    MySQL 存储过程,游标,临时表创建
    List connected users–similar to task manager
    游戏视频编辑
    游戏音频编辑
    UE4 AI BehaviorTree 动画播放完成通知机制
    UE4 AI BehaviorTree 各个节点执行顺序总结
    Holographic Remoting
    Hololens 手势事件执行顺序
    Hololens 硬件细节 Hardware Detail
  • 原文地址:https://www.cnblogs.com/zdd-java/p/10694607.html
Copyright © 2020-2023  润新知