SPI:Java的高可扩展利器

本文分享自华为云社区《一文讲透Java核心技术之高可扩展利器SPI-云社区-华为云》,作者: 冰 河 。
SPI的概念
JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制
SPI的使用场景
Java是一种面向对象语言,虽然Java8开始支持函数式编程和,但是总体来说,还是面向对象的语言 。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码 。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同 。为了使调用方在调用接口的时候 , 明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明 , 这就需要一种服务发现机制 。Java中的SPI加载机制能够满足这样的需求 , 它能够自动寻找某个接口的实现类 。
大量的框架使用了Java的SPI技术,如下:
(1)JDBC加载不同类型的数据库驱动
(2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
(3)中大量使用了SPI
(4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现
SPI的使用
当服务的提供者java中xml怎么提示,提供了接口的一种实现后,需要在Jar包的**META-INF//**目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名) 。
当外部程序通过java.util.类装载这个接口时,就能够通过该Jar包的**META//**目录里的配置文件找到具体的实现类名,装载实例化,完成注入 。同时java中xml怎么提示,SPI的规范规定了接口的实现类必须有一个无参构造方法 。
SPI中查找接口的实现类是通过java.util.,而在java.util.类中有一行代码如下:
// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下private static final String PREFIX = "META-INF/services/";
这也就是说,我们必须将接口的配置文件写到Jar包的**META//**目录下 。
SPI实例
这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类 。
注意:实例是基于Java8进行开发的 。
1.创建Maven项目
在IDEA中创建Maven项目spi-demo , 如下:
2.编辑pom.xml
spi-demo io.binghe.spijar1.0.0-SNAPSHOT4.0.0org.springframework.bootspring-boot-maven-pluginorg.apache.maven.pluginsmaven-compiler-plugin3.6.01.81.8
3.创建类加载工具类
在io..spi.包下创建,类中直接调用JDK的类加载Class 。代码如下所示 。
package io.binghe.spi.loader;import java.util.ServiceLoader;/** * @author binghe * @version 1.0.0 * @description 类加载工具 */public class MyServiceLoader {/*** 使用SPI机制加载所有的Class*/public staticServiceLoader loadAll(final Class clazz) {return ServiceLoader.load(clazz);}}
4.创建接口
在io..spi.包下创建接口,作为测试接口,接口中只有一个方法,打印传入的字符串信息 。代码如下所示:
package io.binghe.spi.service;/** * @author binghe * @version 1.0.0 * @description 定义接口 */public interface MyService {/***打印信息*/void print(String info);}
5.创建接口的实现类(1)创建第一个实现类
在io..spi..impl包下创建类,实现接口 。代码如下所示:
package io.binghe.spi.service.impl;import io.binghe.spi.service.MyService;/** * @author binghe * @version 1.0.0 * @description 接口的第一个实现 */public class MyServiceA implements MyService {@Overridepublic void print(String info) {System.out.println(MyServiceA.class.getName() + " print " + info);}}
(2)创建第二个实现类
在io..spi..impl包下创建类,实现接口 。代码如下所示:
package io.binghe.spi.service.impl;import io.binghe.spi.service.MyService;/** * @author binghe * @version 1.0.0 * @description 接口第二个实现 */public class MyServiceB implements MyService {@Overridepublic void print(String info) {System.out.println(MyServiceB.class.getName() + " print " + info);}}
6.创建接口文件
在项目的src/main/目录下创建**META//**目录,在目录中创建io..spi..文件,注意:文件必须是接口的全名,之后将实现接口的类配置到文件中,如下所示:
io.binghe.spi.service.impl.MyServiceAio.binghe.spi.service.impl.MyServiceB
7.创建测试类
在项目的io..spi.main包下创建Main类,该类为测试程序的入口类 , 提供一个main()方法 , 在main()方法中调用类加载接口的实现类 。并通过Java8的将结果打印出来 , 如下所示:
package io.binghe.spi.main;import io.binghe.spi.loader.MyServiceLoader;import io.binghe.spi.service.MyService;import java.util.ServiceLoader;import java.util.stream.StreamSupport;/** * @author binghe * @version 1.0.0 * @description 测试的main方法 */public class Main {public static void main(String[] args){ServiceLoader loader = MyServiceLoader.loadAll(MyService.class);StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));}}
8.测试实例
运行Main类中的main()方法,打印出的信息如下所示:
io.binghe.spi.service.impl.MyServiceA print Hello Worldio.binghe.spi.service.impl.MyServiceB print Hello WorldProcess finished with exit code 0
通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法 。
源码解析
这里 , 主要是对SPI的加载流程涉及到的java.util.的源码的解析 。
进入java.util.的源码 , 可以看到类实现了java.lang.接口 , 如下所示 。
public final class ServiceLoaderimplements Iterable
说明类是可以遍历迭代的 。
java.util.类中定义了如下的成员变量:
// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下private static final String PREFIX = "META-INF/services/";// 需要加载的接口private final Class service;// 类加载器,用于加载以接口命名的文件中配置的接口的实现类private final ClassLoader loader;// 创建ServiceLoader时采用的访问控制上下文环境private final AccessControlContext acc;// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名 , Value为实现类对象private LinkedHashMap providers = new LinkedHashMap();// 用于延迟加载实现类的迭代器private LazyIterator lookupIterator;
可以看到类中定义了加载前缀为“META-INF//” , 所以,接口文件必须要在项目的src/main/目录下的**META-INF//**目录下创建 。
从类调用**.load(clazz)**方法进入源码,如下所示:
//根据类的Class对象加载指定的类,返回ServiceLoader对象public staticServiceLoader load(Class service) { //获取当前线程的类加载器 ClassLoader cl = Thread.currentThread().getContextClassLoader(); //动态加载指定的类 , 将类加载到ServiceLoader中 return ServiceLoader.load(service, cl);}
方法中调用了**.load(, cl)**方法,继续跟踪代码,如下所示:
//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中public staticServiceLoader load(Class service, ClassLoader loader){ return new ServiceLoader(service, loader);}
可以看到**.load(, cl)**方法中,调用了类的构造方法,继续跟进代码 , 如下所示:
//构造ServiceLoader对象private ServiceLoader(Class svc, ClassLoader cl) { //如果传入的Class对象为空 , 则判处空指针异常 service = Objects.requireNonNull(svc, "Service interface cannot be null"); //如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取 , 否则直接使用传入的ClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload();}
继续跟**()**方法,如下所示 。
//重新加载public void reload() { //清空保存加载的实现类的LinkedHashMap providers.clear(); //构造延迟加载的迭代器 lookupIterator = new LazyIterator(service, loader);}
继续跟进懒加载迭代器的构造函数,如下所示 。
private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader;}
可以看到,会将需要加载的接口的Class对象和类加载器赋值给的成员变量 。
当我们在程序中迭代获取对象实例时,首先在成员变量**中查找是否有缓存的实例对象 。如果存在则直接返回,否则调用**延迟加载迭代器进行加载 。
迭代器进行逻辑判断的代码如下所示:
//迭代ServiceLoader的方法public Iterator iterator() { return new Iterator() {//获取保存实现类的LinkedHashMap的迭代器Iterator<Map.Entry> knownProviders = providers.entrySet().iterator();//判断是否有下一个元素public boolean hasNext() {//如果knownProviders存在元素,则直接返回trueif (knownProviders.hasNext())return true;//返回延迟加载器是否存在元素return lookupIterator.hasNext();}//获取下一个元素public S next() {//如果knownProviders存在元素 , 则直接获取if (knownProviders.hasNext())return knownProviders.next().getValue();//获取延迟迭代器lookupIterator中的元素return lookupIterator.next();}public void remove() {throw new UnsupportedOperationException();} };}
加载类的流程如下代码所示
//判断是否拥有下一个实例private boolean hasNextService() { //如果拥有下一个实例,直接返回true if (nextName != null) {return true; } //如果实现类的全名为null if (configs == null) {try {//获取全文件名 , 文件相对路径+文件名称(包名+接口名)String fullName = PREFIX + service.getName();//类加载器为空,则通过ClassLoader.getSystemResources()方法获取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()) {//如果configs中没有更过的元素,则直接返回falseif (!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 {//通过c.newInstance()生成对象实例S p = service.cast(c.newInstance());//将生成的对象实例保存到缓存中(LinkedHashMap)providers.put(cn, p);return p; } catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x); } throw new Error();// This cannot happen}public boolean hasNext() { if (acc == null) {return hasNextService(); } else {PrivilegedAction action = new PrivilegedAction() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc); }}public S next() { if (acc == null) {return nextService(); } else {PrivilegedAction action = new PrivilegedAction() {public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc); }}
最后,给出整个java.util.的类,如下所示:
package java.util;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.net.URL;import java.security.AccessControlContext;import java.security.AccessController;import java.security.PrivilegedAction;public final class ServiceLoaderimplements Iterable {// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下private static final String PREFIX = "META-INF/services/";// 需要加载的接口private final Class service;// 类加载器,用于加载以接口命名的文件中配置的接口的实现类private final ClassLoader loader;// 创建ServiceLoader时采用的访问控制上下文环境private final AccessControlContext acc;// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象private LinkedHashMap providers = new LinkedHashMap();// 用于延迟加载实现类的迭代器private LazyIterator lookupIterator;//重新加载public void reload() {//清空保存加载的实现类的LinkedHashMapproviders.clear();//构造延迟加载的迭代器lookupIterator = new LazyIterator(service, loader);}//构造ServiceLoader对象private ServiceLoader(Class svc, ClassLoader cl) {//如果传入的Class对象为空,则判处空指针异常service = Objects.requireNonNull(svc, "Service interface cannot be null");//如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获?。?裨蛑苯邮褂么?氲腃lassLoaderloader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload();}private static void fail(Class service, String msg, Throwable cause)throws ServiceConfigurationError{throw new ServiceConfigurationError(service.getName() + ": " + msg,cause);}private static void fail(Class service, String msg)throws ServiceConfigurationError{throw new ServiceConfigurationError(service.getName() + ": " + msg);}private static void fail(Class service, URL u, int line, String msg)throws ServiceConfigurationError{fail(service, u + ":" + line + ": " + msg);}// Parse a single line from the given configuration file, adding the name// on the line to the names list.//private int parseLine(Class service, URL u, BufferedReader r, int lc,List 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('t') >= 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);}if (!providers.containsKey(ln) && !names.contains(ln))names.add(ln);}return lc + 1;}private Iterator parse(Class service, URL u)throws ServiceConfigurationError{InputStream in = null;BufferedReader r = null;ArrayList 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();}// Private inner class implementing fully-lazy provider lookuploadprivate class LazyIteratorimplements Iterator{Class service;ClassLoader loader;Enumeration configs = null;Iterator pending = null;String nextName = null;private LazyIterator(Class service, ClassLoader loader) {this.service = service;this.loader = loader;}//判断是否拥有下一个实例private boolean hasNextService() {//如果拥有下一个实例 , 直接返回trueif (nextName != null) {return true;}//如果实现类的全名为nullif (configs == null) {try {//获取全文件名,文件相对路径+文件名称(包名+接口名)String fullName = PREFIX + service.getName();//类加载器为空,则通过ClassLoader.getSystemResources()方法获取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()) {//如果configs中没有更过的元素,则直接返回falseif (!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 {//通过c.newInstance()生成对象实例S p = service.cast(c.newInstance());//将生成的对象实例保存到缓存中(LinkedHashMap)providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();// This cannot happen}public boolean hasNext() {if (acc == null) {return hasNextService();} else {PrivilegedAction action = new PrivilegedAction() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}}public S next() {if (acc == null) {return nextService();} else {PrivilegedAction action = new PrivilegedAction() {public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc);}}public void remove() {throw new UnsupportedOperationException();}}//迭代ServiceLoader的方法public Iterator iterator() {return new Iterator() {//获取保存实现类的LinkedHashMap的迭代器Iterator<Map.Entry> knownProviders = providers.entrySet().iterator();//判断是否有下一个元素public boolean hasNext() {//如果knownProviders存在元素,则直接返回trueif (knownProviders.hasNext())return true;//返回延迟加载器是否存在元素return lookupIterator.hasNext();}//获取下一个元素public S next() {//如果knownProviders存在元素,则直接获取if (knownProviders.hasNext())return knownProviders.next().getValue();//获取延迟迭代器lookupIterator中的元素return lookupIterator.next();}public void remove() {throw new UnsupportedOperationException();}};}//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中public staticServiceLoader load(Class service,ClassLoader loader){return new ServiceLoader(service, loader);}//根据类的Class对象加载指定的类,返回ServiceLoader对象public staticServiceLoader load(Class service) {//获取当前线程的类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();//动态加载指定的类,将类加载到ServiceLoader中return ServiceLoader.load(service, cl);}public staticServiceLoader loadInstalled(Class service) {ClassLoader cl = ClassLoader.getSystemClassLoader();ClassLoader prev = null;while (cl != null) {prev = cl;cl = cl.getParent();}return ServiceLoader.load(service, prev);}/*** Returns a string describing this service.** @returnA descriptive string*/public String toString() {return "java.util.ServiceLoader[" + service.getName() + "]";}}
SPI总结
最后,对Java提供的SPI机制进行简单的总结 。
优点:
能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离 , 而不是耦合在一起 。应用程序可以根据实际业务情况启用框架扩展或替换框架组件 。
缺点:
参考:深入理解Java中的spi机制
点击下方,第一时间了解华为云新鲜技术~
华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云
【SPI:Java的高可扩展利器】本文到此结束,希望对大家有所帮助 。