SPI服务发现机制

SPI是Java JDK内部提供的一种服务发现机制。
  • SPI->Service Provider Interface,服务提供接口,是Java JDK内置的一种服务发现机制
  • 通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类
[⚠️注意事项]:
面向对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行编码。如果涉及实现类就会违反可插拔的原则,针对于模块装配,Java SPI提供了为某个接口寻找服务的实现机制。

SPI规范

  • 使用约定:

[1].编写服务提供接口,可以是抽象接口和函数接口,JDK1.8之后推荐使用函数接口

[2].在jar包的META-INF/services/目录里创建一个以服务接口命名的文件。其实就是实现该服务接口的具体实现类。

提供一个目录:META-INF/services/放到ClassPath下面

[3].当外部程序装配这个模块的时候,就能通过该Jar包META-INF/services/配置文件找到具体的实现类名,并装载实例化,完成模块注入。

目录下放置一个配置文件:文件名是需要拓展的接口全限定名称文件内部为要实现的接口实现类文件必须为UTF-8编码

[4].寻找服务接口实现,不用在代码中提供,而是利用JDK提供服务查找工具类:java.util.ServiceLoader类来加载使用:

ServiceLoader.load(xxx.class)ServiceLoader<XXXInterface> loads = ServiceLoader.load(xxx.class)

SPI源码分析

[1].ServiceLoader源码:

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.AccessController;import java.security.AccessControlContext;import java.security.PrivilegedAction;import java.util.ArrayList;import java.util.Enumeration;import java.util.Iterator;import java.util.List;import java.util.NoSuchElementException;public final class ServiceLoader<S> implements Iterable<S>{    //[1].初始化定义全局配置文件路径Path    private static final String PREFIX = "META-INF/services/";    //[2].初始化定义加载的服务类或接口    private final Class<S> service;    //[3].初始化定义类加载器    private final ClassLoader loader;    //[4].初始化定义访问控制上下文    private final AccessControlContext acc;    //[5].初始化定义加载服务类的缓存集合     private LinkedHashMap<String,S> providers = new LinkedHashMap<>();    //[6].初始化定义私有内部LazyIterator类,真正加载服务类的实现类    private LazyIterator lookupIterator;        //私有化有参构造-> ServiceLoader(Class<S> svc, ClassLoader cl)    private ServiceLoader(Class<S> svc, ClassLoader cl) {   //[1].实例化服务接口->Class<S>    service = Objects.requireNonNull(svc, "Service interface cannot be null");    //[2].实例化类加载器->ClassLoader    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;    //[3].实例化访问控制上下文->AccessControlContext    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;    //[4].回调函数->reload    reload();    }        public void reload() {    //[1].清空缓存实例集合    providers.clear();    //[2].实例化私有内部LazyIterator类->LazyIterator    lookupIterator = new LazyIterator(service, loader);    }        public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader)    {     return new ServiceLoader<>(service, loader);    }        public static <S> ServiceLoader<S> load(Class<S> service) {      ClassLoader cl = Thread.currentThread().getContextClassLoader();      return ServiceLoader.load(service, cl);    }      }

2.LazyIterator源码:

  private class LazyIterator implements Iterator<S> {    Class<S> service;    ClassLoader loader;    Enumeration<URL> configs = null;    Iterator<String> pending = null;    String nextName = null;    private LazyIterator(Class<S> service, ClassLoader loader) {      this.service = service;      this.loader = loader;    }    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(); // This cannot happen    }    public boolean hasNext() {      if (acc == null) {        return hasNextService();      } else {        PrivilegedAction<Boolean> action =            new PrivilegedAction<Boolean>() {              public Boolean run() {                return hasNextService();              }            };        return AccessController.doPrivileged(action, acc);      }    }    public S next() {      if (acc == null) {        return nextService();      } else {        PrivilegedAction<S> action =            new PrivilegedAction<S>() {              public S run() {                return nextService();              }            };        return AccessController.doPrivileged(action, acc);      }    }    public void remove() {      throw new UnsupportedOperationException();    }  }

使用举例

[1].Dubbo SPI 机制:

META-INF/dubbo.internal/xxx=接口全限定名

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。
Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下。
与Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。
[2].Cache SPI 机制:

  META-INF/service/javax.cache.spi.CachingProvider=xxx

[3]Spring SPI 机制:

META-INF/services/org.apache.commons.logging.LogFactory=xxx

[4].SpringBoot SPI机制:

META-INF/spring.factories/org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx

在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回
源码:

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";// spring.factories文件的格式为:key=value1,value2,value3// 从所有的jar包中找到META-INF/spring.factories文件// 然后从文件中解析出key=factoryClass类名称的所有value值public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {    String factoryClassName = factoryClass.getName();    // 取得资源文件的URL    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));    List<String> result = new ArrayList<String>();    // 遍历所有的URL    while (urls.hasMoreElements()) {        URL url = urls.nextElement();        // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));        String factoryClassNames = properties.getProperty(factoryClassName);        // 组装数据,并返回        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));    }    return result;}

[5].自定义序列化实现SPI:META-INF/services/xxx=接口全限定名

参考学习Java SPI 和Dubbo SPI机制源码,自己动手实现序列化工具类等

版权声明:本文为博主原创文章,遵循相关版权协议,如若转载或者分享请附上原文出处链接和链接来源。