SPI机制:服务扩展的核心技术

发布时间:2026/6/30 23:39:11
SPI机制:服务扩展的核心技术 为什么需要SPI机制SPI和API的区别是什么SPI是一种跟API相对应的反向设计思想API由实现方确定标准规范和功能调用方无权做任何干预 而SPI是由调用方确定标准规范也就是接口然后调用方依赖此接口第三方实现此接口这样做就可以方便的进行扩展类似于插件机制这是SPI出现的需求背景。SPI “接口”位于“调用方”所在的“包”中概念上更依赖调用方。组织上位于调用方所在的包中。实现位于独立的包中。常见的例子是插件模式的插件。API “接口”位于“实现方”所在的“包”中概念上更接近实现方。组织上位于实现方所在的包中。实现和接口在一个包中。什么是SPI机制SPIService Provider Interface是JDK内置的一种 服务提供发现机制可以用来启用框架扩展和替换组件主要是被框架的开发人员使用例如数据库中的java.sql.Driver接口不同的厂商可以针对同一接口做出不同的实现如下图所示MySQL和PostgreSQL都有不同的实现提供给用户。而Java的SPI机制可以为某个接口寻找服务实现Java中SPI机制主要思想是将装配的控制权移到程序之外在模块化设计中这个机制尤其重要其核心思想就是解耦。SPI整体机制图如下当服务的提供者提供了一种接口的实现之后需要在classpath下的 META-INF/services/ 目录里创建一个文件文件名是以服务接口命名的而文件里的内容是这个接口的具体的实现类。当其他的程序需要这个服务的时候就可以通过查找这个jar包一般都是以jar包做依赖的META-INF/services/中的配置文件配置文件中有接口的具体实现类名再根据这个类名进行加载实例化就可以使用该服务了。JDK中查找服务的实现的工具类是java.util.ServiceLoader。SPI机制的简单示例假设现在需要一个发送消息的服务MessageService发送消息的实现可能是基于短信、也可能是基于电子邮件、或推送通知发送消息。接口定义首先定义一个接口MessageServicejavapublic interface MessageService { void sendMessage(String message); }提供两个实现类一个通过短信发送消息一个通过电子邮件发送消息。java// 短信发送实现 public class SmsMessageService implements MessageService { Override public void sendMessage(String message) { System.out.println(Sending SMS: message); } } // 电子邮件发送实现 public class EmailMessageService implements MessageService { Override public void sendMessage(String message) { System.out.println(Sending Email: message); } }配置文件在META-INF/services/目录下创建一个配置文件文件名为MessageService全限定名com.example.MessageService文件内容为接口的实现类的全限定名。java# 文件: META-INF/services/com.seven.MessageService com.seven.SmsMessageService com.seven.EmailMessageService加载服务实现在应用程序中通过ServiceLoader动态加载并使用这些实现类。javapublic class Application { public static void main(String[] args) { ServiceLoaderMessageService loader ServiceLoader.load(MessageService.class); for (MessageService service : loader) { service.sendMessage(Hello, SPI!); } } }运行时ServiceLoader会发现并加载配置文件中列出的所有实现类并依次调用它们的sendMessage方法。由于在 配置文件 写了两个实现类因此两个实现类都会执行 sendMessage 方法。这就是因为ServiceLoader.load(Search.class)在加载某接口时会去 META-INF/services 下找接口的全限定名文件再根据里面的内容加载相应的实现类。这就是spi的思想接口的实现由provider实现provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件并添加进相应的实现类内容就好。SPI机制的应用JDBC DriverManager在JDBC4.0之前开发连接数据库的时候通常会用Class.forName(com.mysql.jdbc.Driver)这句先加载数据库相关的驱动然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(com.mysql.jdbc.Driver)来加载驱动直接获取连接就可以了原因就是现在使用了Java的SPI扩展机制来实现。如上图所示首先在java中定义了接口 java.sql.Driver并没有具体的实现具体的实现都是由不同厂商来提供的。在mysql的jar包mysql-connector-java-8.0.26.jar中可以找到 META-INF/services 目录该目录下会有一个名字为 java.sql.Driver 的文件文件内容是com.mysql.cj.jdbc.Driver这里面的内容就是mysql针对Java中定义的接口的实现。同样在ojdbc的jar包ojdbc11.jar中也可以找到同样的配置文件文件内容是 oracle.jdbc.OracleDriver这是oracle数据库对Java的java.sql.Driver的实现。使用方法而现在Java中写连接数据库的代码的时候不需要再使用Class.forName(com.mysql.jdbc.Driver)来加载驱动了直接获取连接就可以了javaString url jdbc:xxxx://xxxx:xxxx/xxxx; Connection conn DriverManager.getConnection(url, username, password); .....这里并没有涉及到spi的使用看下面源码。源码实现上面的使用方法就是普通的连接数据库的代码实际上并没有涉及到 SPI 的东西但是有一点可以确定的是我们没有写有关具体驱动的硬编码Class.forName(com.mysql.jdbc.Driver)而上面的代码就可以直接获取数据库连接进行操作但是跟SPI有啥关系呢既然上面代码没有加载驱动的代码那实际上是怎么去确定使用哪个数据库连接的驱动呢这里就涉及到使用Java的SPI 扩展机制来查找相关驱动的东西了关于驱动的查找其实都在DriverManager中DriverManager是Java中的实现用来获取数据库连接源码如下javapublic class DriverManager { // 存放注册的jdbc驱动 private final static CopyOnWriteArrayListDriverInfo registeredDrivers new CopyOnWriteArrayList(); /** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {code ServiceLoader} mechanism */ static { loadInitialDrivers(); println(JDBC DriverManager initialized); } private static void loadInitialDrivers() { String drivers; try { // 从JVM -D参数读取jdbc驱动 drivers AccessController.doPrivileged(new PrivilegedActionString() { public String run() { return System.getProperty(jdbc.drivers); } }); } catch (Exception ex) { drivers null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() AccessController.doPrivileged(new PrivilegedActionVoid() { public Void run() { ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class); IteratorDriver driversIterator loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but its * packaged as service and that service is there in classpath. */ try{ // 加载创建所有Driver while(driversIterator.hasNext()) { // 触发Driver的类加载-在静态代码块中创建Driver对象并放到DriverManager driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println(DriverManager.initialize: jdbc.drivers drivers); if (drivers null || drivers.equals()) { return; } // 解析JVM参数的jdbc驱动 String[] driversList drivers.split(:); println(number of Drivers: driversList.length); for (String aDriver : driversList) { try { println(DriverManager.Initialize: loading aDriver); // initial为ture // 触发Driver的类加载-在静态代码块中创建Driver对象并放到DriverManager Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println(DriverManager.Initialize: load failed: ex); } } } }上面的代码主要步骤是从系统变量中获取有关驱动的定义。使用SPI来获取驱动的实现。遍历使用SPI获取到的具体实现实例化各个实现类。根据第一步获取到的驱动列表来实例化具体实现类。第二步使用SPI来获取驱动的实现对应的代码是javaServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class);这里封装了接口类型和类加载器并初始化了一个迭代器。第三步遍历获取到的具体实现实例化各个实现类对应的代码如下java//获取迭代器 IteratorDriver driversIterator loadedDrivers.iterator(); //遍历所有的驱动实现 while(driversIterator.hasNext()) { driversIterator.next(); }在遍历的时候首先调用driversIterator.hasNext()方法这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件并找到文件中的实现类的名字此时并没有实例化具体的实现类ServiceLoader具体的源码实现在下面。然后是调用driversIterator.next();方法此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。Common-Loggingcommon-logging也称Jakarta Commons Logging缩写 JCL是常用的日志库门面 使用了SPI的方式来动态加载和配置日志实现。这种机制允许库在运行时找到合适的日志实现而无需硬编码具体的日志库。我们看下它是怎么通过SPI解耦的。首先日志实例是通过LogFactory的getLog(String)方法创建的javapublic static getLog(Class clazz) throws LogConfigurationException { return getFactory().getInstance(clazz); }LogFatory是一个抽象类它负责加载具体的日志实现getFactory()方法源码如下javapublic static org.apache.commons.logging.LogFactory getFactory() throws LogConfigurationException { // Identify the class loader we will be using ClassLoader contextClassLoader getContextClassLoaderInternal(); if (contextClassLoader null) { // This is an odd enough situation to report about. This // output will be a nuisance on JDK1.1, as the system // classloader is null in that environment. if (isDiagnosticsEnabled()) { logDiagnostic(Context classloader is null.); } } // Return any previously registered factory for this class loader org.apache.commons.logging.LogFactory factory getCachedFactory(contextClassLoader); if (factory ! null) { return factory; } if (isDiagnosticsEnabled()) { logDiagnostic( [LOOKUP] LogFactory implementation requested for the first time for context classloader objectId(contextClassLoader)); logHierarchy([LOOKUP] , contextClassLoader); } // classpath根目录下寻找commons-logging.properties Properties props getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES); // Determine whether we will be using the thread context class loader to // load logging classes or not by checking the loaded properties file (if any). // classpath根目录下commons-logging.properties是否配置use_tccl ClassLoader baseClassLoader contextClassLoader; if (props ! null) { String useTCCLStr props.getProperty(TCCL_KEY); if (useTCCLStr ! null) { if (Boolean.valueOf(useTCCLStr).booleanValue() false) { baseClassLoader thisClassLoader; } } } // 这里真正开始决定使用哪个factory // 首先尝试查找vm系统属性org.apache.commons.logging.LogFactory其是否指定factory if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] Looking for system property [ FACTORY_PROPERTY ] to define the LogFactory subclass to use...); } try { String factoryClass getSystemProperty(FACTORY_PROPERTY, null); if (factoryClass ! null) { if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] Creating an instance of LogFactory class factoryClass as specified by system property FACTORY_PROPERTY); } factory newFactory(factoryClass, baseClassLoader, contextClassLoader); } else { if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] No system property [ FACTORY_PROPERTY ] defined.); } } } catch (SecurityException e) { if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] A security exception occurred while trying to create an instance of the custom factory class : [ trim(e.getMessage()) ]. Trying alternative implementations...); } // ignore } catch (RuntimeException e) { if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] An exception occurred while trying to create an instance of the custom factory class : [ trim(e.getMessage()) ] as specified by a system property.); } throw e; } // 第二尝试使用java spi服务发现机制在META-INF/services下寻找org.apache.commons.logging.LogFactory实现 if (factory null) { if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] Looking for a resource file of name [ SERVICE_ID ] to define the LogFactory subclass to use...); } try { // META-INF/services/org.apache.commons.logging.LogFactory, SERVICE_ID final InputStream is getResourceAsStream(contextClassLoader, SERVICE_ID); if (is ! null) { // This code is needed by EBCDIC and other strange systems. // Its a fix for bugs reported in xerces BufferedReader rd; try { rd new BufferedReader(new InputStreamReader(is, UTF-8)); } catch (java.io.UnsupportedEncodingException e) { rd new BufferedReader(new InputStreamReader(is)); } String factoryClassName rd.readLine(); rd.close(); if (factoryClassName ! null !.equals(factoryClassName)) { if (isDiagnosticsEnabled()) { logDiagnostic([LOOKUP] Creating an instance of LogFactory class factoryClassName as specified by file SERVICE_ID which was present in the path of the context classloader.); } factory newFactory(factoryClassName, baseClassLoader, contextClassLoader); } } else { // is null if (isDiagnosticsEnabled()) {