概述 SPI全称叫做 service provider interface,是Jdk提供的供第三方的用来扩展已有服务的一种方案,通过这种机制我们将接口和实现分开,
并可以在外部灵活的装配实现,这样说可能有点拗口,简单点说就是在面向接口编程的过程中提供了注入具体实现 的策略。其核心类是ServiceLoader
,该类会去默认指定的路径下加载对应的实现,并通过反射的方式来生成实现类的对象(多说一句,既然是通过反射的方式来生成对应的实现类的对象,那么
可定是要求实现类提供一个默认的构造器,接下来可以验证以下)
代码及流程演示 定义接口及实现 接口如下
1 2 3 4 public interface Person { void say () ; }
我们接下来定义两个实现,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Teacher implements Person { @Override public void say () { System.out.println("i'm a teacher !" ); } } public class Student implements Person { @Override public void say () { System.out.println("i'm a student" ); } }
实现发现 对于服务的发现我们需要在classpath下新建一个目录,路径为:META-INF/services,并在该路径下新建一个接口的全限定名文件,
然后将该接口对应的实现类的全限定名写入,如下:
验证结果 接下来我们就可以通过测试来验证SPI机制了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.ServiceLoader;public class SPITest { public static void main (String[] args) { ServiceLoader<Person> peoples = ServiceLoader.load(Person.class ) ; for (Person p : peoples) { p.say(); } } }
上面我们说过serviceLoader在完成类的加载之后,会通过反射的方式来生成实现类对应的对象,因此需要一个默认的构造器,
我们可以验证一下,如下给Student类提供一个含参的构造器:
1 2 3 4 5 6 7 8 9 10 public class Student implements Person { private int name; public Student (int name) { this .name = name; } 、、、、 }
接下来我们再测试一下就会报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Exception in thread "main" java.util.ServiceConfigurationError: com.h3c.Person: Provider com.h3c.Student could not be instantiated at java.util.ServiceLoader.fail(ServiceLoader.java:232) at java.util.ServiceLoader.access$100(ServiceLoader.java:185) at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:384) at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404) at java.util.ServiceLoader$1.next(ServiceLoader.java:480) at com.h3c.SPITest.main(SPITest.java:10) Caused by: java.lang.InstantiationException: com.h3c.Student at java.lang.Class.newInstance(Class.java:427) at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380) ... 3 more Caused by: java.lang.NoSuchMethodException: com.h3c.Student.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.newInstance(Class.java:412) ... 4 more
因此这也验证了我们上面的猜想是正确的。
原理说明 我们在调用 ServiceLoader.load()方法的时候,会构建出一个新的serviceLoader对象,而该对象内部封装了一个内部类LazyIterator,通过
类的名称我们可以知道这个类为一个迭代器,而且应该具备lazy的机制,也就是在我们生成serviceloader对象的时候,并不会为我们提供的实现类生成对象,
而只有在遍历的时候才会初始化,核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 private boolean hasNextService () { if (nextName != null ) { return true ; } 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 ; } 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(); }
这里PREFIX就是我们存放接口全路径的文件夹的地址,通过这段代码我们可以知道,在遍历的时候我们会访问指定路径下的文件,并通过反射的方式生成对象
,然后将类和对应的对象缓存起来。
使用场景 数据库驱动的加载,class.forName(“com.mysql.jdbc.Driver”),待完善
总结 使用serviceLoader可以将服务的装配与调用分离,实现业务的解偶,不过多线程的情况下是线程不安全的。
参考链接:https://www.cnkirito.moe/spi/