java开发ServiceLoader实现机制及SPI应用

作者:梦想实现家_Z 时间:2022-12-24 09:55:05 

前言

做过java web开发的小伙伴大多数时候都需要链接数据库,这个时候就需要配置数据库引擎DriverClassName参数,这样我们的java应用才能通过数据库厂商给的Driver与指定的数据库建立通信。但是这里就有一个疑问:

java.sql.Driver是jdk自带的接口,它是由BoostrapClassLoader加载的,DriverClassName是外部厂商提供的具体实现,是由AppClassLoader加载的,要建立与数据库的通信,必然是通过java.sql.Driver接口方法发起的,那么在java.sql.Driver是如何拿到具体实现的呢?它是不是违背了ClassLoader的双亲委派模式呢?

如何绕过双亲委派模式

为了拿到AppClassLoader中加载的java.sql.Driver实现类,我们可以查看一下DriverManager是怎么处理的:

public static Driver getDriver(String url)
       throws SQLException {
   println("DriverManager.getDriver("" + url + "")");
   ensureDriversInitialized();
   ......
}
private static void ensureDriversInitialized() {
   ......
   AccessController.doPrivileged(new PrivilegedAction<Void>() {
               public Void run() {
                   // 核心代码ServiceLoader
                   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;
               }
           });
   ......
}

我们最终可以发现,DriverManager通过ServiceLoader.load(Driver.class)就拿到了我们配置的DriverClassName实现类。这就实现在DriverManager中拿到了外部提供的Driver实现,绕过来双亲委派模式。

ServiceLoader实现机制

我们来看一下ServiceLoader是如何实现SPI机制的,先从ServiceLoader.load()方法入手:

public static <S> ServiceLoader<S> load(Class<S> service) {
       // 从当前线程中获取ClassLoader
       ClassLoader cl = Thread.currentThread().getContextClassLoader();
       // 创建一个ServiceLoader对象
       return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
   }

1.从当前线程中获取ClassLoader;因为在创建AppClassLoader后,将AppClassLoader设置进当前线程的上下文中;

2.根据ClassLoader以及目标接口类创建一个ServiceLoader对象;

其实ServiceLoader核心代码在hasNext()方法中:

@Override
       public boolean hasNext() {
           if (acc == null) {
               return hasNextService();
           } else {
               PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
                   public Boolean run() { return hasNextService(); }
               };
               return AccessController.doPrivileged(action, acc);
           }
       }

最终都会调用到hasNextService()方法中:

private boolean hasNextService() {
   // nextProvider默认为null,如果通过next()取出来了,nextProvider就会变成null
   while (nextProvider == null && nextError == null) {
       try {
           // 找到目标实现类
           Class<?> clazz = nextProviderClass();
           if (clazz == null)
               return false;
           if (clazz.getModule().isNamed()) {
               // ignore class if in named module
               continue;
           }
           // 判断service接口是否和clazz有父子关系
           if (service.isAssignableFrom(clazz)) {
               Class<? extends S> type = (Class<? extends S>) clazz;
               // 获取无参构造函数
               Constructor<? extends S> ctor
                           = (Constructor<? extends S>)getConstructor(clazz);
               // 包装成一个ProviderImpl对象
               ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
               // 并赋值给nextProvider
               nextProvider = (ProviderImpl<T>) p;
           } else {
               fail(service, clazz.getName() + " not a subtype");
           }
       } catch (ServiceConfigurationError e) {
           nextError = e;
       }
   }
   return true;
}

外部提供的实现类一定要有一个无参构造函数,否则会导致ServiceLoader加载失败;

我们下面再继续深入看看ServiceLoader是怎么找到实现类的:

private Class<?> nextProviderClass() {
   if (configs == null) {
       try {
           // 拼接文件名:"META-INF/services/接口名称"
           // 比如接口名为:java.sql.Driver,
           // 那么文件路径就是:"META-INF/services/java.sql.Driver"
           String fullName = PREFIX + service.getName();
           // 没有指定ClassLoader,就通过getSystemClassLoader()加载目标文件
           if (loader == null) {
               configs = ClassLoader.getSystemResources(fullName);
           } else if (loader == ClassLoaders.platformClassLoader()) {
               // 如果是platformClassLoader,它没有class path,那么看看BootLoader有没有class path
               if (BootLoader.hasClassPath()) {
                   configs = BootLoader.findResources(fullName);
               } else {
                   configs = Collections.emptyEnumeration();
               }
           } else {
               // 通过指定classLoader加载目标文件
               configs = loader.getResources(fullName);
           }
       } catch (IOException x) {
           fail(service, "Error locating configuration files", x);
       }
   }
   // 上面代码只会执行一次,这样configs就不会为null,下次进来直接取下一个实现类
   // 把configs内容解析成一个迭代器
   while ((pending == null) || !pending.hasNext()) {
       if (!configs.hasMoreElements()) {
           return null;
       }
       pending = parse(configs.nextElement());
   }
   // 通过迭代器获取下一个实现类名称
   String cn = pending.next();
   try {
       // 通过类名反射成Class对象
       return Class.forName(cn, false, loader);
   } catch (ClassNotFoundException x) {
       fail(service, "Provider " + cn + " not found");
       return null;
   }
}

1.实现类的载入是因为在META-INF/services/文件夹中创建了以目标接口名称命名的文件,并在里面写上了实现类的全路径类名。

2.ServiceLoader通过ClassLoader从class path中载入目标文件里面的内容,并解析出实现类的全路径类名;

3.最终通过反射的方式创建出实现类的Class对象,这样就完成了SPI的实现;

SPI在各个框架上的应用

除了在数据库Driver上使用了SPI,我们还可以发现SPI在各个框架上都有大量的应用。比如我最近在看的Seata分布式事务框架,里面就有用到SPIio.seata.common.loader.EnhancedServiceLoader

java开发ServiceLoader实现机制及SPI应用

另一个就是我们经常使用的mysql-connector-java以及阿里的Druid:

java开发ServiceLoader实现机制及SPI应用

java开发ServiceLoader实现机制及SPI应用

小结

通过以上源码分析以及示例演示,我们简单做一个小结:

1.ServiceLoader打破双亲委派模式的方式通过获取当前线程上下文中的ClassLoader完成的;

2.SPI的实现类名称必须放在META-INF/services/文件夹下面,以目标接口名称作为文件名称,文件内容为目标实现类全路径类名;

3.目标实现类必须要有一个无参构造函数,否则SPI会失败;

来源:https://juejin.cn/post/7159095184674783239

标签:java,ServiceLoader,SPI
0
投稿

猜你喜欢

  • Java并发编程之ConcurrentLinkedQueue源码详解

    2023-01-22 16:19:51
  • JavaSE-面向对象(方法重写)

    2023-01-27 10:51:09
  • Java中Date日期时间类具体使用

    2022-04-11 23:18:13
  • Java深入讲解二十三种设计模式之中的策略模式

    2022-11-25 13:23:15
  • Android编程实现拍照功能的2种方法分析

    2023-10-16 19:02:25
  • java 中newInstance()方法和new关键字的区别

    2023-11-25 07:17:26
  • 学习Java的Date、Calendar日期操作

    2023-09-04 22:26:38
  • java面向对象之人机猜拳小游戏

    2021-12-20 18:12:34
  • 如何在android中使用html作布局文件

    2023-01-14 07:49:10
  • Android集成Flutter

    2023-07-06 13:07:33
  • Android实现遮罩层(蒙板)效果

    2023-04-26 18:43:03
  • Android编程实现二维码的生成与解析

    2021-12-08 23:13:15
  • Spring自动装配之方法、构造器位置的自动注入操作

    2021-11-30 23:28:40
  • 使用genymotion访问本地上Tomcat上数据的方法

    2022-11-23 05:51:43
  • springboot打包部署到linux服务器的方法

    2021-09-26 14:56:14
  • Java案例之随机验证码功能实现实例

    2022-05-24 13:28:16
  • Java SSH 秘钥连接mysql数据库的方法

    2022-07-11 21:23:18
  • 使用Jackson反序列化遇到的问题及解决

    2023-11-13 21:12:14
  • Android实现串口通信

    2023-03-06 18:13:10
  • 使用android隐藏api实现亮度调节的方法

    2022-10-16 23:37:36
  • asp之家 软件编程 m.aspxhome.com