跳至主要內容

SpringBoot-SpringSPI机制

holic-x...大约 6 分钟JAVA框架

SpringBoot-SpringSPI机制

学习核心

  • Java的SPI机制(复习)
  • SpringbootSPI机制

学习资料

SPI机制

​ 首先对Java的SPI机制进行复盘,在理解的基础上再去拆解Springboot的SPI机制原理(其实在解析Springboot自动装配和启动流程的时候就已经涉及到这块的内容)

​ SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要用于框架中开发,例如Dubbo、Spring、Common-Logging,JDBC等采用采用SPI机制,针对同一接口采用不同的实现提供给不同的用户,从而提高了框架的扩展性

Java中的SPI机制实现

​ Java内置的SPI通过java.util.ServiceLoader类解析classPath和jar包的META-INF/services/目录下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用。关注其核心实现ServiceLoader源码:

public final class ServiceLoader<S>
    implements Iterable<S>
{

    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator
      
      .........
}
  • ServiceLoader类本身实现了Iterable接口并实现了其中的iterator方法,iterator方法的实现中调用了LazyIterator这个内部类中的方法,迭代器创建实例
  • 所有服务提供接口的对应文件都是放置在META-INF/services/目录下,final类型决定了PREFIX目录不可变更

​ Java提供的SPI机制思想很好,但是也存在相应的弊端:

  • 无法按需加载:必须依次遍历、实例化所有的实现类,才能获取到想要的方法调用
  • 所有的服务接口必须放在META-INF/services/目录下按照指定规则进行配置

​ 针对Java的SPI机制存在的问题,Spring的SPI机制则是沿用SPI的思想,对其进行扩展和优化,采用spring.factories的方式实现SPI机制,可以在不修改Spring源码的前提提供Spring框架的扩展性(可以结合Springboot自动装配和启动流程原理分析,结合自定义扩展的一些案例去理解)

SPI案例分析

1.Java的SPI机制

构建思路

  • 创建接口(Video)和对应实现类(TencentVideo、AqyVideo)
  • 在resources/META-INF/services下创建一个名为【Video的全限定类名称】的文件,内容为对应的接口实现的全限定类名称(多个实现分行定义)
  • 创建JavaSPITest类测试

步骤1

// 定义Video接口
public interface Video {
    void play();
}

public class TencentVideo implements Video{
    @Override
    public void play() {
        System.out.println("tencent video play");
    }
}

public class AqyVideo implements Video{
    @Override
    public void play() {
        System.out.println("AqyVideo video play");
    }
}

步骤2

​ 在resources/META-INF/services下创建一个名为 [com.noob.framework.spi.java.Video]文件,内容定义为对应的接口实现的全限定类名称(多个实现分行定义)

com.noob.framework.spi.java.TencentVideo
com.noob.framework.spi.java.AqyVideo

步骤3

// JavaSPI机制测试
public class JavaSPITest {
    public static void main(String[] args) {
        // 加载所有的实现并执行
        ServiceLoader<Video> serviceLoader = ServiceLoader.load(Video.class);
        for (Video video : serviceLoader) {
            System.out.println(video);
            // 例如此处想要摘选TencentVideo
            if(TencentVideo.class.isInstance(video)){
                TencentVideo tencentVideo = (TencentVideo) video;
                tencentVideo.play();
            }
        }
    }
}

// output
com.noob.framework.spi.java.TencentVideo@42a57993
tencent video play
com.noob.framework.spi.java.AqyVideo@75b84c92

2.Springboot的SPI机制

构建思路

  • 创建接口(DataBase)和对应实现类(OracleDataBase、MySQLDataBase)
  • 在resources/META-INF下创建一个名为【spring.factories】的文件,内容采用key、value形式存储对应结合和关联实现类定义
  • 创建SpringbootSPITest类测试

步骤1

// 定义数据库接口
public interface DataBase {
    public void initLink();
}

public class OracleDataBase implements DataBase{
    @Override
    public void initLink() {
        System.out.println("oracle database initLink");
    }
}

public class MySQLDataBase implements DataBase{
    @Override
    public void initLink() {
        System.out.println("mysql database initLink");
    }
}

步骤2

​ 在resources/META-INF下创建一个名为【spring.factories】的文件,内容采用key、value形式存储对应结合和关联实现类定义

​ 例如此处接口为DataBase,对应多个实现用逗号分隔

com.noob.framework.spi.springboot.DataBase=\
  com.noob.framework.spi.springboot.MySQLDataBase,com.noob.framework.spi.springboot.OracleDataBase

步骤3

// SpringbootSPI机制测试
public class SpringbootSPITest {
    public static void main(String[] args) {
        List<DataBase> dataBaseList = SpringFactoriesLoader.loadFactories(DataBase.class, Thread.currentThread().getContextClassLoader());
        for (DataBase dataBase : dataBaseList) {
            System.out.println(dataBase);
            // 摘选Oracle数据库方法执行
            if(OracleDataBase.class.isInstance(dataBase)){
                OracleDataBase oracleDataBase = (OracleDataBase) dataBase;
                oracleDataBase.initLink();
            }
        }
    }
}

// output
com.noob.framework.spi.springboot.MySQLDataBase@5c0369c4
com.noob.framework.spi.springboot.OracleDataBase@2be94b0f
oracle database initLink

3.案例对比分析

​ 结合上述案例分析可知,两种机制的应用流程非常类似,但是很明显Springboot对Java原生的SPI机制做了优化:

  • 配置文件整合

    • Springboot将所有的配置都定义在一个文件spring.factories中,通过key、value的形式来绑定不同接口及其实现
    • Java原生SPi机制则是将所有配置都放在META/services目录下,每一个接口都对应一个配置文件来绑定实现类

Springboot SPI机制源码剖析

// 核心加载代码
SpringFactoriesLoader.loadFactories(DataBase.class, Thread.currentThread().getContextClassLoader());

​ 进入loadFactories方法拆解源码分析

public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
  Assert.notNull(factoryType, "'factoryType' must not be null");
  // 确定类加载器
  ClassLoader classLoaderToUse = classLoader;
  if (classLoaderToUse == null) {
    classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
  }

  // 核心逻辑1:解析和加载META-INF下的文件(可以进一步跟踪源码追踪到loadSpringFactories方法)
  List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
  if (logger.isTraceEnabled()) {
    logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
  }

  List<T> result = new ArrayList(factoryImplementationNames.size());
  Iterator var5 = factoryImplementationNames.iterator();

  while(var5.hasNext()) {
    String factoryImplementationName = (String)var5.next();
    // 核心逻辑2:根据获取到的类限定名称进行实例化操作
    result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
  }

  AnnotationAwareOrderComparator.sort(result);
  return result;
}


// --------------------- 核心源码拆解 ---------------------
# 1.跟踪loadFactoryNames方法定义
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  ClassLoader classLoaderToUse = classLoader;
  if (classLoaderToUse == null) {
    classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
  }

  String factoryTypeName = factoryType.getName();
  return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

# 2.进一步跟踪loadSpringFactories方法实现
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
  Map<String, List<String>> result = (Map)cache.get(classLoader);
  if (result != null) {
    return result;
  } else {
    Map<String, List<String>> result = new HashMap();

    try {
      Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");

      while(urls.hasMoreElements()) {
        URL url = (URL)urls.nextElement();
        UrlResource resource = new UrlResource(url);
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        Iterator var6 = properties.entrySet().iterator();

        while(var6.hasNext()) {
          Map.Entry<?, ?> entry = (Map.Entry)var6.next();
          String factoryTypeName = ((String)entry.getKey()).trim();
          String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
          String[] var10 = factoryImplementationNames;
          int var11 = factoryImplementationNames.length;

          for(int var12 = 0; var12 < var11; ++var12) {
            String factoryImplementationName = var10[var12];
            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
              return new ArrayList();
            })).add(factoryImplementationName.trim());
          }
        }
      }

      result.replaceAll((factoryType, implementations) -> {
        return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
      });
      cache.put(classLoader, result);
      return result;
    } catch (IOException var14) {
      IOException ex = var14;
      throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);
    }
  }
}

​ 核心步骤1:拆解loadSpringFactories核心实现,该方法主要是用于获取所有jar包中META-INF/spring.factories文件路径,以枚举值返回。

image-20240613203227174

​ 核心步骤2:遍历spring.factories文件路径,逐个加载解析,整合factoryClass类型的实现类名称,获取到实现类的全类名称后进行类的实例话操作,拆解instantiateFactory方法实现,其相关源码如下,其原理是通过反射来进行对象的实例化

private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
  try {
    Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
    if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
      throw new IllegalArgumentException("Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
    } else {
      return ReflectionUtils.accessibleConstructor(factoryImplementationClass, new Class[0]).newInstance();
    }
  } catch (Throwable var4) {
    Throwable ex = var4;
    throw new IllegalArgumentException("Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", ex);
  }
}
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3