文章目录
- 一、先搞懂:SPI到底是干啥的?
- 二、最基础:JDK原生Java SPI
- 1、原生SPI核心规则
- 2、Java SPI完整实战示例
- ① 第一步:定义支付统一接口
- ② 第二步:写两个接口实现类
- ③ 第三步:创建SPI配置文件(关键)
- ④ 第四步:ServiceLoader加载测试
- 3、运行结果 \& 原生SPI致命缺点
- 三、进阶版:Spring SPI(改良增强版Java SPI)
- 1、Spring SPI核心改动
- 2、Spring SPI快速演示(简单易懂)
- ① 新建spring\.factories配置文件
- ② SpringFactoriesLoader加载测试
- 四、重点核心:SpringBoot新版SPI(现在项目主流用法)
- 1、关键知识点:新旧版本区别
- 2、SpringBoot SPI核心规则
- 3、SpringBoot SPI完整实战
- ① 第一步:新建SpringBoot项目,无需额外依赖
- ② 定义短信服务统一接口
- ③ 编写两个实现类
- ④ 新建SpringBoot新版SPI核心配置文件
- ⑤ 启动类测试注入使用
- 4、运行效果
- 五、三者核心区别一张表看懂(面试必背)
- 六、实际开发中SPI到底有啥用?
- 七、最后简单总结
很多后端小伙伴面试总被问:Java SPI是什么?Spring SPI和原生SPI有啥区别?SpringBoot自动配置为啥靠SPI就能不用手动new对象?
平时写业务代码,咱们天天@Autowired注入Bean,习惯了Spring容器帮我们管理所有对象。但大家有没有想过一个问题:
Spring框架本身、SpringBoot各种starter,压根不知道我们后续会写什么业务实现,为啥启动就能自动加载各种扩展功能、自动装配配置类?
答案核心就两个字:SPI。
今天从JDK原生SPI讲起,再到Spring SPI,最后落地SpringBoot新版SPI实战,由浅入深,每个都给完整可运行demo,看完不仅懂原理,还知道工作中啥时候用、怎么用。
一、先搞懂:SPI到底是干啥的?
先别记英文全称,不用背官方术语。
SPI本质就一件事:接口和实现解耦,不用改核心代码,通过配置就能切换、新增插件式实现。
咱们平时开发是:写接口 → 写实现类 → 代码里直接new或者注入实现,写死了,换个实现就要改代码重启。
SPI模式是:
只定义统一接口(定规范)
多个不同业务写不同实现(做插件)
配置文件里声明用哪个实现(配关系)
框架/程序运行时自动扫描配置,加载对应的实现类
一句话总结:接口我定义,实现别人写,配置配一下,自动就加载。
这就是所有SPI的核心思想,不管是Java原生、Spring还是SpringBoot,底层逻辑全是这个,只是用法和优化程度不一样。
二、最基础:JDK原生Java SPI
1、原生SPI核心规则
JDK自带SPI,不用引任何依赖,开箱即用,规则就三条,很好记:
第一步:定义统一业务接口
第二步:写多个不同的实现类
第三步:在固定目录META-INF/services/接口全类名建配置文件,文件里写实现类全路径
第四步:用JDK自带
ServiceLoader加载实现类,自动实例化调用
重点硬性要求:实现类必须有无参构造方法,不然反射实例化会报错。
2、Java SPI完整实战示例
咱们做一个简单场景:支付方式接口,两种实现:微信支付、支付宝支付,用SPI动态加载。
① 第一步:定义支付统一接口
// 支付业务统一接口publicinterfacePayService{// 统一下单支付方法voidpay(longmoney);}② 第二步:写两个接口实现类
// 微信支付实现publicclassWechatPayServiceImplimplementsPayService{@Overridepublicvoidpay(longmoney){System.out.println("微信支付成功,支付金额:"+money+"元");}}// 支付宝支付实现publicclassAlipayServiceImplimplementsPayService{@Overridepublicvoidpay(longmoney){System.out.println("支付宝支付成功,支付金额:"+money+"元");}}③ 第三步:创建SPI配置文件(关键)
在项目resources目录下,新建文件夹:META\-INF/services。
新建文件,文件名必须是接口的全类名:比如com\.example\.spi\.PayService。
文件内容:写入两个实现类的全类名,换行分隔:
com.example.spi.WechatPayServiceImpl com.example.spi.AlipayServiceImpl④ 第四步:ServiceLoader加载测试
importjava.util.ServiceLoader;publicclassJavaSpiTest{publicstaticvoidmain(String[]args){// 核心:JDK原生ServiceLoader加载接口所有配置的实现类ServiceLoader<PayService>serviceLoader=ServiceLoader.load(PayService.class);// 遍历所有实现,直接调用方法for(PayServicepayService:serviceLoader){payService.pay(99);}}}3、运行结果 & 原生SPI致命缺点
运行输出:
微信支付成功,支付金额:99元 支付宝支付成功,支付金额:99元效果看着还行,但实际开发压根没人直接用Java原生SPI,缺点太致命:
一次性加载所有实现类,不能按需加载,不管用不用都实例化,浪费内存
没有依赖注入,实现类不能管理Bean,没法整合Spring
不能按名称、条件精准选择某个实现,只能全遍历
报错不友好,排查问题极其麻烦
所以Spring看不下去了,自己搞了一套Spring SPI,优化原生所有痛点。
三、进阶版:Spring SPI(改良增强版Java SPI)
1、Spring SPI核心改动
Spring SPI底层思想和Java原生一模一样,还是接口+配置文件+自动加载,只是改了两个核心点,更好用、适配Spring:
配置文件路径改了:老式
META\-INF/spring\.factories(SpringBoot2.7之前核心)加载工具类换成:
SpringFactoriesLoader支持按需加载、Spring容器整合、优先级排序,功能更强
核心作用:Spring底层扩展、框架初始化、加载内置组件全靠它。
2、Spring SPI快速演示(简单易懂)
接口和实现类不用改,还是上面的PayService、微信、支付宝支付实现。
① 新建spring.factories配置文件
resources下新建META\-INF/spring\.factories,格式是key=value:
# key:接口全类名,value:多个实现类全类名逗号分隔 com.example.spi.PayService=\ com.example.spi.WechatPayServiceImpl,\ com.example.spi.AlipayServiceImpl② SpringFactoriesLoader加载测试
importorg.springframework.core.io.support.SpringFactoriesLoader;importjava.util.List;publicclassSpringSpiTest{publicstaticvoidmain(String[]args){// Spring SPI核心加载器List<PayService>payServices=SpringFactoriesLoader.loadFactories(PayService.class,null);for(PayServicepayService:payServices){payService.pay199);}}}运行效果和原生一样,但Spring SPI可以后续结合Spring容器管理,支持Bean注入,扩展性强很多。
四、重点核心:SpringBoot新版SPI(现在项目主流用法)
1、关键知识点:新旧版本区别
SpringBoot 2.7版本是分水岭:
2.7之前:用spring.factories老式SPI
2.7及以后:废弃spring.factories,改用新版SPI配置
新版SpringBoot SPI最大优势:专门给自动配置、插件扩展量身定做,结构更清晰,性能更好,按需加载。
2、SpringBoot SPI核心规则
配置文件改路径:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件里直接写需要自动加载的配置类/扩展类全路径
SpringBoot启动时自动读取,自动注册为Bean,无需手动@Bean
3、SpringBoot SPI完整实战
场景:做一个短信发送SPI扩展,默认阿里云短信,后续随时加腾讯云短信,不用改核心代码。
① 第一步:新建SpringBoot项目,无需额外依赖
② 定义短信服务统一接口
publicinterfaceSmsService{voidsendSms(Stringphone,Stringcontent);}③ 编写两个实现类
// 阿里云短信实现publicclassAliSmsServiceImplimplementsSmsService{@OverridepublicvoidsendSms(Stringphone,Stringcontent){System.out.println("阿里云短信发送成功,手机号:"+phone+",内容:"+content);}}// 腾讯云短信实现publicclassTencentSmsServiceImplimplementsSmsService{@OverridepublicvoidsendSms(Stringphone,Stringcontent){System.out.println("腾讯云短信发送成功,手机号:"+phone+",内容:"+content);}}④ 新建SpringBoot新版SPI核心配置文件
在resources下创建目录:META\-INF/spring。
新建文件:org\.springframework\.boot\.autoconfigure\.AutoConfiguration\.imports。
文件内容:写入需要自动加载的实现类全路径:
com.example.springspi.service.AliSmsServiceImpl com.example.springspi.service.TencentSmsServiceImpl⑤ 启动类测试注入使用
importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;importjava.util.Map;@SpringBootApplicationpublicclassSpringBootSpiApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextcontext=SpringApplication.run(SpringBootSpiApplication.class,args);// 从Spring容器中获取所有SmsService接口的实现BeanMap<String,SmsService>beanMap=context.getBeansOfType(SmsService.class);// 遍历调用beanMap.values().forEach(smsService->smsService.sendSms("13800138000","您的验证码是888888"));}}4、运行效果
阿里云短信发送成功,手机号:13800138000,内容:您的验证码是888888 腾讯云短信发送成功,手机号:13800138000,内容:您的验证码是888888看到没?我们没有给实现类加任何@Component、@Bean注解,SpringBoot启动自动通过SPI配置加载,注册成Bean,直接就能注入使用。
五、三者核心区别一张表看懂(面试必背)
| SPI类型 | 配置文件位置 | 加载工具 | 特点 | 使用场景 |
|---|---|---|---|---|
| Java原生SPI | META-INF/services/接口名 | ServiceLoader | 全加载、无依赖、功能弱 | 简单底层扩展,极少业务用 |
| Spring老式SPI | META-INF/spring.factories | SpringFactoriesLoader | 适配Spring、支持排序 | 旧版SpringBoot自动配置 |
| SpringBoot新版SPI | META-INF/spring/xxx.imports | SpringBoot启动器自动加载 | 性能好、按需加载、专门适配自动配置 | 现在所有SpringBoot项目扩展、starter开发 |
六、实际开发中SPI到底有啥用?
不用觉得SPI只是面试题,工作中天天都在用,只是你没感知:
SpringBoot自动配置:不用手动配Tomcat、Mvc、数据源,全靠SPI自动加载配置类
自定义Starter开发:自己写通用组件,别的项目引入就自动生效,靠SPI
插件化业务扩展:支付、短信、OSS存储多厂商切换,不用改代码,改配置就行
框架中间件扩展:MyBatis、Redis、MQ各种扩展实现,都是SPI思想
七、最后简单总结
所有SPI核心思想都一样:接口定义,实现分离,配置驱动,自动加载。
Java原生SPI基础垫底,缺点多,业务基本不用。
Spring SPI做了优化,适配Spring容器,旧版SpringBoot在用。
现在开发直接学SpringBoot新版SPI,适配最新版本,做自动配置、自定义Starter必备。
SPI终极目的:解耦代码,不用改核心逻辑,就能无限扩展功能。