SPI机制

打印 上一主题 下一主题

主题 916|帖子 916|积分 2748

SPI机制

该项目地址:代码仓库
【项目里面的 spi开头的项目】
1. java的spi机制

SPI (Service Provider Interface)是 Java 尺度中一种服务发现机制,允许在运行时动态地发现和加载服务实现类,而无需在编译时显式绑定。它广泛用于 Java 生态中(如 JDBC 驱动加载、日志框架等)。
SPI 的核心思想


  • 解耦接口与实现:定义一个公共接口(Service Provider Interface),差别的厂商或模块可以实现该接口的差别版本。
  • 动态加载:运行时通过配置文件(META-INF/services/接口全限定名)找到全部实现类,并加载它们。
  • 扩展性强:无需修改主程序代码即可添加新的服务实现。
②经典案例:JDBC 驱动加载

JDBC 驱动的加载是 SPI 的典范应用:

  • 接口:java.sql.Driver
  • 实现类:各个数据库厂商的驱动(如 MySQL 的 com.mysql.cj.jdbc.Driver)
  • 配置文件:META-INF/services/java.sql.Driver 中列出全部驱动类名。
③自定义案例

不修改代码,根据加载的包差别,调用差别的实现方式。
先定义接口提供者
创建项目spi-provider
里面只定义一个接口 UserService
  1. // spi对外暴露的接口
  2. public interface UserService {
  3.     String hello(String name);
  4. }
复制代码
然后创建两个差别的实现

  • 创建项目spi-impl-mysql,引入spi-provider
  1. public class MysqlUserImpl implements UserService {
  2.     @Override
  3.     public String hello(String name) {
  4.         return "【MySQL】:" + name;
  5.     }
  6. }
复制代码
在该项目标resources目次下面创建如下目次
  1. resources
  2. --META-INF
  3. ----services
  4. ------com.feng.spi.UserService [这个是文件]
复制代码
文件的内容是
  1. com.feng.impl.MysqlUserImpl
复制代码

  • 创建项目spi-impl-redis,引入spi-provider
  1. public class RedisUserImpl implements UserService {
  2.     @Override
  3.     public String hello(String name) {
  4.         return "【Redis】 " + name;
  5.     }
  6. }
复制代码
在该项目标resources目次下面创建如下目次
  1. resources
  2. --META-INF
  3. ----services
  4. ------com.feng.spi.UserService [这个是文件]
复制代码
文件的内容是
  1. com.feng.impl.RedisUserImpl
复制代码
创建新项目测试
创建spi-use项目
  1. // 用这个类加载
  2. public class UserServer {
  3.     private static final List<UserService> services = new ArrayList<>();
  4.     private static final UserServer userServer = new UserServer();
  5.     private UserServer(){
  6.         ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); // 加载实现类
  7.         for (UserService userService : userServices) {
  8.             services.add(userService);
  9.         }
  10.     }
  11.     public static String hello(String name){
  12.         if (services.isEmpty()) {
  13.             // System.err.println("No UserService implementation found");
  14.             return "No UserService implementation found";
  15.         }
  16.         return services.get(0).hello(name);
  17.     }
  18. }
  19. // 用这个类运行测试
  20. public class App {
  21.     public static void main(String[] args) {
  22.         String hello = UserServer.hello("田小锋");
  23.         System.err.println(hello);
  24.     }
  25. }
复制代码
第一步,spi-use先引入mysql的项目
  1. <dependency>
  2.     <groupId>com.feng.impl</groupId>
  3.     <artifactId>spi-impl-mysql</artifactId>
  4.     <version>1.0-SNAPSHOT</version>
  5. </dependency>
复制代码
运行测试:
输出效果是:【MySQL】:田小锋
第二步, 将mysql依赖注释掉,引入redis的
  1. <dependency>
  2.     <groupId>com.feng.impl</groupId>
  3.     <artifactId>spi-impl-redis</artifactId>
  4.     <version>1.0-SNAPSHOT</version>
  5. </dependency>
复制代码
运行测试:
输出效果是:【Redis】 田小锋
pom文件依赖发生更改后,一定要记得reload一下pom文件哦。
如许就实现了,更改依赖,我们并没有更改java代码,就实现了导入差别场景,就可以实现差别的功能了。有没有点像SpringBoot的意思了。
通过上面的自定义案例,我们可以总结出java的spi机制的实现方式:

  • 定义接口
  • 提供实现
  • 配置文件:META-INF/services/借口全限定名(文件),文件内容是详细实现类的全限定名
  • 利用、加载服务
④java spi原理

非常轻易就可以看出来,下面这几行代码是核心。
  1. ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); // 加载实现类
  2. for (UserService userService : userServices) {
  3.     services.add(userService);
  4. }
复制代码
从头开始,一步一步分析其源码
  1. public final class ServiceLoader<S> implements Iterable<S> {
  2. ....
  3. }
复制代码
调用了load(Class service)方法
  1. public static <S> ServiceLoader<S> load(Class<S> service) {
  2.     // 获取当前线程的上下文类加载器
  3.     ClassLoader cl = Thread.currentThread().getContextClassLoader();
  4.     // 调用重载的 load 方法
  5.     return ServiceLoader.load(service, cl);
  6. }
复制代码
调用重载的load(Class service, ClassLoader loader)方法
  1. public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
  2.     return new ServiceLoader<>(service, loader); // new 一个对象
  3. }
复制代码
构造函数ServiceLoader(Class svc, ClassLoader cl)
  1. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  2.     // 判断null
  3.     service = Objects.requireNonNull(svc, "Service interface cannot be null");
  4.     // 传过来的上下文类加载器如果是null,就用系统类加载器
  5.     loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  6.     // 这个不是很了解,对于本文章来说不重要
  7.     acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  8.     // 这个才是重点!!!!!!!!!
  9.     reload();
  10. }
复制代码
reload()方法
  1. public void reload() {
  2.     providers.clear();
  3.     lookupIterator = new LazyIterator(service, loader); // 创建对象,就没了=====
  4. }
复制代码
但是哦,ServiceLoader实现了Iterable接口,那么在for遍历的时间,就会隐式调用iterator()获取迭代器。
来看看ServiceLoader实现的iterator()方法里面
  1. public Iterator<S> iterator() {
  2.     return new Iterator<S>() {
  3.         Iterator<Map.Entry<String,S>> knownProviders
  4.             = providers.entrySet().iterator();
  5.         public boolean hasNext() {
  6.             if (knownProviders.hasNext())
  7.                 return true;
  8.             return lookupIterator.hasNext(); // ================
  9.         }
  10.         public S next() {
  11.             if (knownProviders.hasNext())
  12.                 return knownProviders.next().getValue();
  13.             return lookupIterator.next(); // ================
  14.         }
  15.         public void remove() {
  16.             throw new UnsupportedOperationException();
  17.         }
  18.     };
  19. }
复制代码
可以看到在上面reload()里面lookupIterator = new LazyIterator(service, loader);的对象起作用了!
LazyIterator是ServiceLoader的内部类,实现了Iterator接口。接下来就看LazyIterator的hasNext()和next();
  1. private class LazyIterator implements Iterator<S>{
  2.     public boolean hasNext() {
  3.         if (acc == null) {
  4.             return hasNextService();//=========================重点
  5.         } else {
  6.             PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
  7.                 public Boolean run() { return hasNextService();} //=========================重点
  8.             };
  9.             ...........
  10.         }
  11.     }
  12.     public S next() {
  13.         if (acc == null) {
  14.             return nextService();//=========================重点
  15.         } else {
  16.             PrivilegedAction<S> action = new PrivilegedAction<S>() {
  17.                 public S run() { return nextService();} //=========================重点
  18.             };
  19.             .....
  20.         }
  21.     }
  22.     // 重点------------------------
  23.     private boolean hasNextService() {
  24.         if (nextName != null) {
  25.             return true;
  26.         }
  27.         if (configs == null) {
  28.             try {
  29.                 // PREFIX = "META-INF/services/"
  30.                 String fullName = PREFIX + service.getName();
  31.                 if (loader == null)
  32.                     configs = ClassLoader.getSystemResources(fullName);
  33.                 else
  34.                     configs = loader.getResources(fullName);
  35.             ...........
  36.         return true;
  37.     }
  38.     //重点----------------------------
  39.     private S nextService() {
  40.         ...
  41.         String cn = nextName;
  42.         nextName = null;
  43.         Class<?> c = null;
  44.         try {
  45.             // 加载
  46.             c = Class.forName(cn, false, loader);
  47.         } catch (ClassNotFoundException x) {
  48.             ....
  49.         }
  50.         .....
  51.         try {
  52.             // c.newInstance()
  53.             S p = service.cast(c.newInstance());
  54.             providers.put(cn, p); // 放到LinkedHashMap
  55.             return p;
  56.         } .....
  57.     }
  58. }
复制代码
三个重要的点:

  • 颠末源码分析。PREFIX = "META-INF/services/",是约定好的。所以要这么写
  • 底层还是反射。c.newInstance()。无参构造方法创建对象。
  • 迭代器的设计模式
那SPI有什么缺点吗?每次加载都要读取文件,可能会有性能问题,不外可能实际影响不大。另外,只能通过无参构造器实例化类,如果实现类需要参数,可能不太方便。还有,如果有多个实现,需要自己选择利用哪一个,ServiceLoader只是简单地迭代全部实现,可能需要通过某些条件判断来选择。
2.SpringBoot的spi机制

在上面自定义案例的那部分,不是说了如许一句话吗?
并没有更改java代码,就实现了导入差别场景,就可以实现差别的功能了。有没有点像SpringBoot的意思了。
没有错!Spring Boot 对 Java 的 SPI 机制进行了深度集成和扩展,形成了自己的主动配置体系(Auto-configuration)。它的核心思想与 Java SPI 类似,但通过注解和条件化编程进一步简化了服务发现和依赖注入的流程。
其提供了一种解耦容器注入的方式,帮助外部包(独立于spring-boot项目)注册Bean到spring boot项目容器中。
SpringBoot的原理分析文章中已经了解到。
在2.x版本中它是扫描的META-INF/spring.factories文件 【在2.7及以后的版本的EnableAutoConfiguration挪到了 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports】。这里只是简述一下。
  1. # spring.factories
  2. # 键:需要被扩展的接口全限定类名
  3. # 值:实现类的全限定类名(多个实现类用逗号分隔)
  4. # 例如
  5. org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
  6. org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
  7. org.springframework.boot.autoconfigure.condition.OnClassCondition,\
  8. org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
复制代码
Spring boot中默认利用了很多factories机制,主要包含:【copy的别人的】

  • ApplicationContextInitializer:用于在spring容器刷新之前初始化Spring ConfigurableApplicationContext的回调接口。
  • ApplicationListener:用于处理容器初始化各个阶段的变乱。
  • AutoConfigurationImportListener:导入配置类的时间,获取类的详细信息(Listener that can be registered with spring.factories to receive details of imported auto-configurations.)。
  • AutoConfigurationImportFilter:用于按条件过滤导入的配置类(Filter that can be registered in spring.factories to limit the auto-configuration classes considered. This interface is designed to allow fast removal of auto-configuration classes before their bytecode is even read.)
  • EnableAutoConfiguration:指定主动加载的配置类列表(Enable auto-configuration of the Spring Application Context, attempting to guess and configure beans that you are likely to need. Auto-configuration classes are usually applied based on your classpath and what beans you have defined. For example, if you have tomcat-embedded.jar on your classpath you are likely to want a TomcatServletWebServerFactory (unless you have defined your own ServletWebServerFactory bean).
  • FailureAnalyzer:在启动时拦截异常并将其转换为易读的消息,并将其包含在FailureAnalysis中。 Spring Boot为应用程序上下文相关异常、JSR-303验证等提供了此类分析器(A FailureAnalyzer is used to analyze a failure and provide diagnostic information that can be displayed to the user.)
  • TemplateAvailabilityProvider:模版引擎配置。(Collection of TemplateAvailabilityProvider beans that can be used to check which (if any) templating engine supports a given view. Caches responses unless the spring.template.provider.cache property is set to false.)
SpringBoot SPI 的核心机制

1. 基于 spring.factories 的服务发现

  • 配置文件位置:META-INF/spring.factories
  • 作用:用于声明 Spring Boot 的主动配置类(@Configuration 类)或 SPI 实现类。
  • 格式:每行以 键=值 情势定义,键通常是接口全限定名,值是实现类的全限定名。
Spring Boot 通过扫描 spring.factories 中的类,并将它们作为主动配置类加载到 Spring 容器中。
2. 条件化注解驱动
Spring Boot 利用 @ConditionalOnMissingBean、@ConditionalOnClass 等注解动态控制主动配置是否生效。
SpringBoot SPI 的工作流程


  • 启动时扫描
    Spring Boot 启动时会通过 SpringFactoriesLoader.loadFactoryClasses() 方法扫描全部类加载器下的 spring.factories 文件。
  • 加载主动配置类
    将 spring.factories 中声明的类实例化为 Configuration 对象,并注册到 Spring 容器中。
  • 条件过滤
    利用 @Conditional 注解(如 @ConditionalOnClass、@ConditionalOnProperty)筛选符合条件的主动配置类。
  • 依赖注入
    将主动配置类中的 @Bean 方法生成的对象注入到 Spring 容器中。
SpringBoot SPI vs Java SPI

特性Spring Boot SPI尺度 Java SPI配置方式通过 spring.factories 文件 + @Configuration通过 META-INF/services/ 配置文件依赖注入主动将实现类注册为 Spring Bean需手动调用 ServiceLoader 并管理实例条件化支持支持丰富的条件注解(如 @ConditionalOnMissingBean)无条件过滤机制生态系统集成深度集成 Spring Boot 的主动配置体系独立于 Spring 框架④自定义案例

见项目里面的【spi-springboot项目】
还是用上面的例子。我们分别创建项目去实现接口。
首先,我们自定义的项目场景,是没有被Spring官方收录的,不在他的主动配置场景包里面,所以,我们只有模仿@SpringBootApplication注解,通过@Import导入bean。
spi-mysql-starter项目
  1. @Service("mysqlUserService")
  2. public class MysqlUserServiceImpl implements UserService {
  3.     @Override
  4.     public String hello(String name) {
  5.         return "【SpringBoot MySQL】:" + name;
  6.     }
  7. } // 定义实现类
复制代码
然后再resources目次下面创建META-INF/spring.factories文件
  1. # 键:需要被扩展的接口全限定类名
  2. # 值:实现类的全限定类名(多个实现类用逗号分隔)
  3. com.feng.spi.UserService=com.feng.spimysql.MysqlUserServiceImpl
复制代码
spi-redis-starter项目
  1. @Service("redisUserService")
  2. public class RedisUserServiceImpl implements UserService {
  3.     @Override
  4.     public String hello(String name) {
  5.         return "【SpringBoot Redis】:" + name;
  6.     }
  7. }
复制代码
同理
  1. com.feng.spi.UserService=com.feng.spiredis.RedisUserServiceImpl
复制代码
spi-springboot-use项目中
引入上面mysql,redis二者之一、大概都引入。
启动类里面看效果
  1. @SpringBootApplication
  2. public class App {
  3.     public static void main(String[] args) {
  4.         ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
  5.         // 引入其中一个
  6.         try{
  7.             UserService bean = context.getBean(UserService.class);
  8.             System.err.println(bean.hello("田小锋"));
  9.             System.err.println("====================");
  10.         } catch (Exception e ) {
  11.             System.err.println("不止一个UserService");
  12.         }
  13.     }
  14. }
复制代码

如上图,先引入mysql的。

接下来,换一个依赖,引入redis的。看下图

如许,一个简单的例子就完成了,导入差别场景,然后可以让容器里面的bean有差别的功能了。
3.参考

【java中的SPI机制】:https://blog.csdn.net/sigangjun/article/details/79071850
Spring的Factories机制介绍】: https://segmentfault.com/a/1190000042247124
deepseek

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

金歌

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表