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

1639 0
王子 2022-11-8 18:04:39 | 显示全部楼层 |阅读模式
目录

    前言如何绕过双亲委派模式ServiceLoader实现机制SPI在各个框架上的应用小结


前言

做过java web开发的小伙伴大多数时候都需要链接数据库,这个时候就需要配置数据库引擎DriverClassName参数,这样我们的java应用才能通过数据库厂商给的Driver与指定的数据库建立通信。但是这里就有一个疑问:
java.sql.Driver是jdk自带的接口,它是由BoostrapClassLoader加载的,DriverClassName是外部厂商提供的具体实现,是由AppClassLoader加载的,要建立与数据库的通信,必然是通过java.sql.Driver接口方法发起的,那么在java.sql.Driver是如何拿到具体实现的呢?它是不是违背了ClassLoader的双亲委派模式呢?

如何绕过双亲委派模式

为了拿到AppClassLoader中加载的java.sql.Driver实现类,我们可以查看一下DriverManager是怎么处理的:
  1. public static Driver getDriver(String url)
  2.         throws SQLException {
  3.     println("DriverManager.getDriver("" + url + "")");
  4.     ensureDriversInitialized();
  5.     ......
  6. }
  7. private static void ensureDriversInitialized() {
  8.     ......
  9.     AccessController.doPrivileged(new PrivilegedAction<Void>() {
  10.                 public Void run() {
  11.                     // 核心代码ServiceLoader
  12.                     ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  13.                     Iterator<Driver> driversIterator = loadedDrivers.iterator();
  14.                     try {
  15.                         while (driversIterator.hasNext()) {
  16.                             driversIterator.next();
  17.                         }
  18.                     } catch (Throwable t) {
  19.                         // Do nothing
  20.                     }
  21.                     return null;
  22.                 }
  23.             });
  24.     ......
  25. }
复制代码
我们最终可以发现,DriverManager通过ServiceLoader.load(Driver.class)就拿到了我们配置的DriverClassName实现类。这就实现在DriverManager中拿到了外部提供的Driver实现,绕过来双亲委派模式。

ServiceLoader实现机制

我们来看一下ServiceLoader是如何实现SPI机制的,先从ServiceLoader.load()方法入手:
  1.     public static <S> ServiceLoader<S> load(Class<S> service) {
  2.         // 从当前线程中获取ClassLoader
  3.         ClassLoader cl = Thread.currentThread().getContextClassLoader();
  4.         // 创建一个ServiceLoader对象
  5.         return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
  6.     }
复制代码
1.从当前线程中获取ClassLoader;因为在创建AppClassLoader后,将AppClassLoader设置进当前线程的上下文中;
2.根据ClassLoader以及目标接口类创建一个ServiceLoader对象;
其实ServiceLoader核心代码在hasNext()方法中:
  1.        @Override
  2.         public boolean hasNext() {
  3.             if (acc == null) {
  4.                 return hasNextService();
  5.             } else {
  6.                 PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
  7.                     public Boolean run() { return hasNextService(); }
  8.                 };
  9.                 return AccessController.doPrivileged(action, acc);
  10.             }
  11.         }
复制代码
最终都会调用到hasNextService()方法中:
  1. private boolean hasNextService() {
  2.     // nextProvider默认为null,如果通过next()取出来了,nextProvider就会变成null
  3.     while (nextProvider == null && nextError == null) {
  4.         try {
  5.             // 找到目标实现类
  6.             Class<?> clazz = nextProviderClass();
  7.             if (clazz == null)
  8.                 return false;
  9.             if (clazz.getModule().isNamed()) {
  10.                 // ignore class if in named module
  11.                 continue;
  12.             }
  13.             // 判断service接口是否和clazz有父子关系
  14.             if (service.isAssignableFrom(clazz)) {
  15.                 Class<? extends S> type = (Class<? extends S>) clazz;
  16.                 // 获取无参构造函数
  17.                 Constructor<? extends S> ctor
  18.                             = (Constructor<? extends S>)getConstructor(clazz);
  19.                 // 包装成一个ProviderImpl对象
  20.                 ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
  21.                 // 并赋值给nextProvider
  22.                 nextProvider = (ProviderImpl<T>) p;
  23.             } else {
  24.                 fail(service, clazz.getName() + " not a subtype");
  25.             }
  26.         } catch (ServiceConfigurationError e) {
  27.             nextError = e;
  28.         }
  29.     }
  30.     return true;
  31. }
复制代码
外部提供的实现类一定要有一个无参构造函数,否则会导致ServiceLoader加载失败;
我们下面再继续深入看看ServiceLoader是怎么找到实现类的:
  1. private Class<?> nextProviderClass() {
  2.     if (configs == null) {
  3.         try {
  4.             // 拼接文件名:"META-INF/services/接口名称"
  5.             // 比如接口名为:java.sql.Driver,
  6.             // 那么文件路径就是:"META-INF/services/java.sql.Driver"
  7.             String fullName = PREFIX + service.getName();
  8.             // 没有指定ClassLoader,就通过getSystemClassLoader()加载目标文件
  9.             if (loader == null) {
  10.                 configs = ClassLoader.getSystemResources(fullName);
  11.             } else if (loader == ClassLoaders.platformClassLoader()) {
  12.                 // 如果是platformClassLoader,它没有class path,那么看看BootLoader有没有class path
  13.                 if (BootLoader.hasClassPath()) {
  14.                     configs = BootLoader.findResources(fullName);
  15.                 } else {
  16.                     configs = Collections.emptyEnumeration();
  17.                 }
  18.             } else {
  19.                 // 通过指定classLoader加载目标文件
  20.                 configs = loader.getResources(fullName);
  21.             }
  22.         } catch (IOException x) {
  23.             fail(service, "Error locating configuration files", x);
  24.         }
  25.     }
  26.     // 上面代码只会执行一次,这样configs就不会为null,下次进来直接取下一个实现类
  27.     // 把configs内容解析成一个迭代器
  28.     while ((pending == null) || !pending.hasNext()) {
  29.         if (!configs.hasMoreElements()) {
  30.             return null;
  31.         }
  32.         pending = parse(configs.nextElement());
  33.     }
  34.     // 通过迭代器获取下一个实现类名称
  35.     String cn = pending.next();
  36.     try {
  37.         // 通过类名反射成Class对象
  38.         return Class.forName(cn, false, loader);
  39.     } catch (ClassNotFoundException x) {
  40.         fail(service, "Provider " + cn + " not found");
  41.         return null;
  42.     }
  43. }
复制代码
1.实现类的载入是因为在META-INF/services/文件夹中创建了以目标接口名称命名的文件,并在里面写上了实现类的全路径类名。
2.ServiceLoader通过ClassLoader从class path中载入目标文件里面的内容,并解析出实现类的全路径类名;
3.最终通过反射的方式创建出实现类的Class对象,这样就完成了SPI的实现;

SPI在各个框架上的应用

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


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





小结

通过以上源码分析以及示例演示,我们简单做一个小结:
1.ServiceLoader打破双亲委派模式的方式通过获取当前线程上下文中的ClassLoader完成的;
2.SPI的实现类名称必须放在META-INF/services/文件夹下面,以目标接口名称作为文件名称,文件内容为目标实现类全路径类名;
3.目标实现类必须要有一个无参构造函数,否则SPI会失败;
以上就是java开发ServiceLoader实现机制及SPI应用的详细内容,更多关于java开发ServiceLoader SPI的资料请关注中国红客联盟其它相关文章!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行