Java中的SPI是怎么一回事

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

我先举例如何使用java的spi。

1.  首先定义一个服务接口,比如LogService.java

1package code.classloader; 2 3/** 4 * 5 * @author dgm 6 * @describe "日志服务接口" 7 * @date 2020年5月22日 8 */ 9public interface LogService { 10 11 void print(String message); 12}

2.  再定义三个LogService接口的实现类

1package code.classloader; 2 3/** 4 * @author dgm 5 * @describe "日志到控制台" 6 * @date 2020年5月22日 7 */ 8public class StdOutLogServiceImpl implements LogService { 9 10 @Override 11 public void print(String message) { 12 // TODO Auto-generated method stub 13 System.out.println(message); 14 System.out.println("写日志到控制台!"); 15 } 16} 17 18 19package code.classloader; 20 21import java.io.BufferedWriter; 22import java.io.File; 23import java.io.FileInputStream; 24import java.io.FileOutputStream; 25import java.io.FileWriter; 26import java.io.IOException; 27import java.nio.MappedByteBuffer; 28import java.nio.channels.FileChannel; 29 30/** 31 * 32 * @author dgm 33 * @describe "日志到文件" 34 * @date 2020年5月22日 35 */ 36public class FileLogServiceImpl implements LogService { 37 38 private static final String FILE_NAME="d://LogService.txt"; 39 @Override 40 public void print(String message) { 41 try { 42 File file = new File(FILE_NAME); 43 FileWriter fw = null; 44 // true:表示是追加的标志 45 fw = new FileWriter(file, true); 46 fw.write(message+"\n"); 47 fw.close(); 48 49 System.out.println(message); 50 System.out.println("写日志入文件!"); 51 } catch (IOException e) { 52 } 53 } 54} 55 56 57package code.classloader; 58 59/** 60 * @author dgm 61 * @describe "写日志入mysql数据库" 62 * @date 2020年5月22日 63 */ 64public class MysqlLogServiceImpl implements LogService { 65 66 @Override 67 public void print(String message) { 68 // TODO Auto-generated method stub 69 System.out.println(message); 70 System.out.println("写日志入数据库"); 71 } 72}

注意:我把三个实现类(StdOutLogServiceImpl.java,FileLogServiceImpl,MysqlLogServiceImpl)一个代码框里了

3.  在项目src目录下新建一个META-INF/services文件夹,然后再新建一个以LogService接口的全限定名命名的文件code.classloader.LogService
,其文件内容为:

1code.classloader.StdOutLogServiceImpl 2code.classloader.FileLogServiceImpl 3code.classloader.MysqlLogServiceImpl

4.  最后我们再新建一个测试类LogClientTest

1package code.test; 2 3import java.util.Iterator; 4import java.util.ServiceLoader; 5 6import code.classloader.LogService; 7 8/** 9 * @author dgm 10 * @describe "" 11 * @date 2020年5月22日 12 */ 13public class LogClientTest { 14 15 public static void main(String[] args) { 16 ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class); 17 Iterator<LogService> it = loader.iterator(); 18 while (it != null && it.hasNext()){ 19 LogService logService = it.next(); 20 logService.print("日志实现是:= " + logService.getClass()); 21 } 22 } 23}

运行测试类,结果如下图所示:

5. Java的SPI机制的源码分析

从测试类LogClientTest我们看到Java的SPI机制实现跟ServiceLoader这个类有关,那么我们先来看下ServiceLoader的类结构代码:

1//注意ServiceLoader类实现了Iterable接口 2publicfinalclass ServiceLoader<S> implements Iterable<S>{ 3 //这下知道为啥要把案例中约束目录固定死了吧 4 private static final String PREFIX = "META-INF/services/"; 5 6 // The class or interface representing the service being loaded 7 private final Class<S> service; 8 // The class loader used to locate, load, and instantiate providers 9 private final ClassLoader loader; 10 // The access control context taken when the ServiceLoader is created 11 private final AccessControlContext acc; 12 // Cached providers, in instantiation order 13 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); 14 // The current lazy-lookup iterator 15 private LazyIterator lookupIterator; 16 // 构造方法 17 private ServiceLoader(Class<S> svc, ClassLoader cl) { 18 service = Objects.requireNonNull(svc, "Service interface cannot be null"); 19 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; 20 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; 21 reload(); 22 } 23 24 // ...暂时省略相关代码 25 26 // ServiceLoader的内部类LazyIterator,实现了【Iterator】接口 27 // Private inner class implementing fully-lazy provider lookup 28 private class LazyIterator 29 implements Iterator<S>{ 30 Class<S> service; 31 ClassLoader loader; 32 Enumeration<URL> configs = null; 33 Iterator<String> pending = null; 34 String nextName = null; 35 36 private LazyIterator(Class<S> service, ClassLoader loader) { 37 this.service = service; 38 this.loader = loader; 39 } 40 // 覆写Iterator接口的hasNext方法 41 public boolean hasNext() { 42 // ...暂时省略相关代码 43 } 44 // 覆写Iterator接口的next方法 45 public S next() { 46 // ...暂时省略相关代码 47 } 48 // 覆写Iterator接口的remove方法 49 public void remove() { 50 // ...暂时省略相关代码 51 } 52 53 } 54 55 // 覆写Iterable接口的iterator方法,返回一个迭代器 56 public Iterator<S> iterator() { 57 // ...暂时省略相关代码 58 } 59 60 // ...暂时省略相关代码 61 62}

这下知道为啥要把案例中约束目录名META-INF/services/固定死了吧。

可以看到,ServiceLoader实现了Iterable接口,覆写其iterator方法能产生一个迭代器;同时ServiceLoader有一个内部类LazyIterator,而LazyIterator又实现了Iterator接口,说明LazyIterator是一个迭代器。

5.1  ServiceLoader.load方法,为加载服务提供者实现类做前期准备

   我们开始探究Java的SPI机制的源码, 先来看LogClientTest的第一句代码

ServiceLoader<LogService> loader = ServiceLoader.load(LogService.class);

ServiceLoader.load(LogService.class)的源码如下:

1// ServiceLoader.java 2public static <S> ServiceLoader<S> load(Class<S> service) { 3 //获取当前线程上下文类加载器 4 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 5 // 将service接口类和线程上下文类加载器作为参数传入,继续调用load方法 6 return ServiceLoader.load(service, cl); 7}

我们继续往下看ServiceLoader.load(service, cl)方法:

1// ServiceLoader.java 2 3public static <S> ServiceLoader<S> load(Class<S> service, 4 ClassLoader loader) 5{ 6 // 将service接口类和线程上下文类加载器作为构造参数,新建了一个ServiceLoader对象 7 return new ServiceLoader<>(service, loader); 8}

继续接着看new ServiceLoader<>(service, loader)是如何构建的?

1// ServiceLoader.java 2 3private ServiceLoader(Class<S> svc, ClassLoader cl) { 4 service = Objects.requireNonNull(svc, "Service interface cannot be null"); 5 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; 6 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; 7 //重点来了 8 reload(); 9}

可以看到在构建ServiceLoader对象时除了给其成员属性赋值外,还调用了reload方法:

1// ServiceLoader.java 2 3public void reload() { 4 providers.clear(); 5 6 lookupIterator = new LazyIterator(service, loader); 7}

可以看到在reload方法中又新建了一个LazyIterator对象,然后赋值给lookupIterator

1// ServiceLoader$LazyIterator.java 2 3private LazyIterator(Class<S> service, ClassLoader loader) { 4 this.service = service; 5 this.loader = loader; 6}

可以看到在构建LazyIterator对象时,也只是给其成员变量serviceloader属性赋值。

5.2  ServiceLoader.iterator方法,实现服务提供者实现类的懒加载

我们现在再来看LogClientTest的第二句代码

Iterator<LogService> it = loader.iterator();

,执行这句代码后最终会调用serviceLoaderiterator方法:

1// serviceLoader.java 2 3public Iterator<S> iterator() { 4 return new Iterator<S>() { 5 6 Iterator<Map.Entry<String,S>> knownProviders 7 = providers.entrySet().iterator(); 8 9 public boolean hasNext() { 10 if (knownProviders.hasNext()) 11 returntrue; 12 // 调用lookupIterator即LazyIterator的hasNext方法 13 // 可以看到是委托给LazyIterator的hasNext方法来实现 14 return lookupIterator.hasNext(); 15 } 16 17 public S next() { 18 if (knownProviders.hasNext()) 19 return knownProviders.next().getValue(); 20 // 调用lookupIterator即LazyIterator的next方法 21 // 可以看到是委托给LazyIterator的next方法来实现 22 return lookupIterator.next(); 23 } 24 25 public void remove() { 26 thrownew UnsupportedOperationException(); 27 } 28 29 }; 30}

可以看到调用serviceLoaderiterator方法会返回一个匿名的迭代器对象,而这个匿名迭代器对象其实相当于一个门面类,其覆写的hasNextnext方法又分别委托LazyIteratorhasNextnext方法来实现了。

我们继续追踪代码,发现接下来会进入LazyIteratorhasNext方法:

1// serviceLoader$LazyIterator.java 2 3public boolean hasNext() { 4 if (acc == null) { 5 // 调用hasNextService方法 6 return hasNextService(); 7 } else { 8 PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { 9 public Boolean run() { return hasNextService(); } 10 }; 11 return AccessController.doPrivileged(action, acc); 12 } 13}

然后继续跟进hasNextService方法:

1// serviceLoader$LazyIterator.java 2 3private boolean hasNextService() { 4 if (nextName != null) { 5 return true; 6 } 7 if (configs == null) { 8 try { 9 // 终于出现约定目录名了,即PREFIX = "META-INF/services/" 10 // service.getName()即接口的全限定名 11 // 还记得前面的代码构建LazyIterator对象时已经给其成员属性service赋值吗 12 String fullName = PREFIX + service.getName(); 13 // 加载META-INF/services/目录下的接口文件中的服务提供者类 14 if (loader == null) 15 configs = ClassLoader.getSystemResources(fullName); 16 else 17 // 还记得前面的代码构建LazyIterator对象时已经给其成员属性loader赋值吗 18 configs = loader.getResources(fullName); 19 } catch (IOException x) { 20 fail(service, "Error locating configuration files", x); 21 } 22 } 23 while ((pending == null) || !pending.hasNext()) { 24 if (!configs.hasMoreElements()) { 25 return false; 26 } 27 // 返回META-INF/services/目录下的接口文件中的服务提供者类并赋值给pending属性 28 pending = parse(service, configs.nextElement()); 29 } 30 // 然后取出一个全限定名赋值给LazyIterator的成员变量nextName 31 nextName = pending.next(); 32 return true; 33}

可以看到在执行LazyIteratorhasNextService方法时最终将去META-INF/services/目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给LazyIterator的成员变量nextName。到了这里,我们就明白了LazyIterator的作用真的是懒加载,在用到的时候才会真正去加载服务提供者实现类。

同样,执行完LazyIteratorhasNext方法后,会继续执行LazyIteratornext方法:

1// serviceLoader$LazyIterator.java 2 3public S next() { 4 if (acc == null) { 5 // 调用nextService方法 6 return nextService(); 7 } else { 8 PrivilegedAction<S> action = new PrivilegedAction<S>() { 9 public S run() { return nextService(); } 10 }; 11 return AccessController.doPrivileged(action, acc); 12 } 13}

我们继续跟进nextService方法:

1// serviceLoader$LazyIterator.java 2 3private S nextService() { 4 if (!hasNextService()) 5 thrownew NoSuchElementException(); 6 // 还记得在hasNextService方法中为nextName赋值过服务提供者实现类的全限定名吗 7 String cn = nextName; 8 nextName = null; 9 Class<?> c = null; 10 try { 11 // 【1】去classpath中根据传入的类加载器和服务提供者实现类的全限定名去加载服务提供者实现类 12 c = Class.forName(cn, false, loader); 13 } catch (ClassNotFoundException x) { 14 fail(service, 15 "Provider " + cn + " not found"); 16 } 17 if (!service.isAssignableFrom(c)) { 18 fail(service, 19 "Provider " + cn + " not a subtype"); 20 } 21 try { 22 // 【2】实例化刚才加载的服务提供者实现类,并进行转换 23 S p = service.cast(c.newInstance()); 24 // 【3】最终将实例化后的服务提供者实现类放进providers集合 25 providers.put(cn, p); 26 return p; 27 } catch (Throwable x) { 28 fail(service, 29 "Provider " + cn + " could not be instantiated", 30 x); 31 } 32 thrownew Error(); // This cannot happen 33}

可以看到LazyIteratornextService方法最终将实例化之前加载的服务提供者实现类,并放进providers集合中,随后再调用服务提供者实现类的方法。注意,这里是加载一个服务提供者实现类后,若main函数中有调用该服务提供者实现类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。

因此,我们看到了ServiceLoader.iterator方法真正承担了加载并实例化META-INF/services/目录下的接口文件里定义的服务提供者实现类。

想了解**SpringBoot的SPI机制的样板,**META-INF/spring.factories,算是一种约定,也可以参考下

SpringBoot扩展点之EnvironmentPostProcessor https://blog.csdn.net/dong19891210/article/details/106436364

总结: 如果你看懂了java的spi,那么spring boot、dubbo的spi也能搞懂了,变体(当看到一样事物的内在逻辑,就要学会润色、加工、处理、改造、完善,灵活变通、举一反三)!!!

附代码目录结构:

参考:

     0.  java.util Class ServiceLoader https://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html

  1. Java是如何实现自己的SPI机制的? JDK源码(一) https://mp.weixin.qq.com/s/6BhHBtoBlSqHlXduhzg7Pw

  2. Spring-SpringFactoriesLoader详解  https://msd.misuland.com/pd/2884250137616453978

  3. 探讨注解驱动Spring应用的机制,详解ServiceLoader、SpringFactoriesLoader的使用(以JDBC、spring.factories为例介绍SPI) https://cloud.tencent.com/developer/article/1497777

  4. Dubbo源码解析之SPI(一):扩展类的加载过程 https://blog.51cto.com/14159827/2475733?source=drh

  5. Java Code Examples for org.springframework.core.io.support.SpringFactoriesLoader  https://www.programcreek.com/java-api-examples/index.php?api=org.springframework.core.io.support.SpringFactoriesLoader

  6. Java Service Loader vs Spring Factories Loader https://blog.frankel.ch/java-service-loader-vs-spring-factories/

  7. JDK的SPI原理及源码分析 https://mp.weixin.qq.com/s?__biz=MzI1MjQ2NjEyNA==&mid=2247483671&idx=1&sn=6d6ea78a1d7fd7ef0fb2a3f948bdca99

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )