Java编程技术之浅析SPI服务发现机制

Wesley13
• 阅读 672

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源码: Java编程技术之浅析SPI服务发现机制

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源码: Java编程技术之浅析SPI服务发现机制

  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机制源码,自己动手实现序列化工具类等

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

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
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
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
可插拔组件设计机制—SPI
SPI的全称是ServiceProviderInterface,即提供服务接口;是一种服务发现机制,SPI的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。本篇文章聚焦SPI的使用场景及使用介绍。
Stella981 Stella981
3年前
Dubbo 扩展点加载机制:从 Java SPI 到 Dubbo SPI
!(https://oscimg.oschina.net/oscnet/up1aa4ada0efc8a144d35d25b3443d951c7e3.JPEG)SPI全称为ServiceProviderInterface,是一种服务发现机制。当程序运行调用接口时,会根据配置文件或默认规则信息加载对应的实现类。所以在程序中并没有直接指定使用接口
Wesley13 Wesley13
3年前
Java中的SPI是怎么一回事
!(https://oscimg.oschina.net/oscnet/up2acf1859e4ef7973a34a3dffe4af94a7b51.png)SPI,全称为ServiceProviderInterface,是一种服务发现机制。它通过在ClassPath路径下的METAINF/services文件夹查找文件,自动加载文件里所定
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这