SpringBoot 自动装配原理,手写 starter 组件
1、先看几个注解
@SpringBootConfiguration 注解
这个注解我们点进去就可以发现,它实际上就是一个 @Configuration 注解,这个注解大家应该很熟悉了,加上这个注解就是为了让当前类作为一个配置类交由 Spring 的 IOC 容器进行管理,因为前面我们说了,SpringBoot 本质上还是 Spring,所以原属于 Spring 的注解 @Configuration 在 SpringBoot 中也可以直接应用。
@ComponentScan 注解
这个注解也很熟悉,用于定义 Spring 的扫描路径,等价于在 xml 文件中配置 context:component-scan,假如不配置扫描路径,那么 Spring 就会默认扫描当前类所在的包及其子包中的所有标注了 @Component,@Service,@Controller 等注解的类。
@EnableAutoConfiguration
这个注解才是实现自动装配的关键,点进去之后发现,它是一个由 @AutoConfigurationPackage 和 @Import 注解组成的复合注解。
@EnableXXX 注解也并不是 SpringBoot 中的新注解,这种注解在 Spring 3.1 版本就开始出现了,比如开启定时任务的注解 @EnableScheduling 等。
@Import 注解
这个注解比较关键,我们通过一个例子来说明一下。
定义一个普通类 TestImport,不加任何注解,我们知道这个时候这个类并不会被 Spring 扫描到,也就是无法直接注入这个类:
public class TestImport {}
现实开发中,假如就有这种情况,定义好了一个类,即使加上了注解,也不能保证这个类一定被 Spring 扫描到,这个时候该怎么做呢?
这时候我们可以再定义一个类 MyConfiguration,保证这个类可以被 Spring 扫描到,然后通过加上 @Import 注解来导入 TestImport 类,这时候就可以直接注入 TestImport 了:
@Configuration@Import(TestImport.class) public class MyConfiguration {}
所以这里的 @Import 注解其实就是为了去导入一个类?AutoConfigurationImportSelector,接下来我们需要分析一下这个类。
AutoConfigurationImportSelector 类
进入这个类之后,有一个方法,这个方法很好理解,首先就是看一下 AnnotationMetadata(注解的元信息),有没有数据,没有就说明没导入直接返回一个空数组,否则就调用 getAutoConfigurationEntry 方法:
进入 getAutoConfigurationEntry 方法:
这个方法里面就是通过调用?getCandidateConfigurations
来获取候选的 Bean,并将其存为一个集合,最后经过去重,校验等一系列操作之后,被封装成 AutoConfigurationEntry
对象返回。
继续进入?getCandidateConfigurations
方法,这时候就几乎看到曙光了:
这里面再继续点击去就没必要了,看错误提示大概就知道了,loadFactoryNames
方法会去 META-INF/spring.factories
文件中根据 EnableAutoConfiguration 的全限定类名获取到我们需要导入的类,而 EnableAutoConfiguration
类的全限定类名为?org.springframework.boot.autoconfigure.EnableAutoConfiguration
,那么就让我们打开这个文件看一下:
可以看到,这个文件中配置了大量的需要自动装配的类,当我们启动 SpringBoot 项目的时候,SpringBoot 会扫描所有 jar 包下面的 META-INF/spring.factories
文件,并根据 key 值进行读取,最后在经过去重等一些列操作得到了需要自动装配的类。
需要注意的是:上图中的 spring.factories 文件是在 spring-boot-autoconfigure
包下面,这个包记录了官方提供的 stater 中几乎所有需要的自动装配类,所以并不是每一个官方的 starter 下都会有 spring.factories 文件。
谈谈 SPI 机制
通过 SpringFactoriesLoader 来读取配置文件 spring.factories 中的配置文件的这种方式是一种 SPI 的思想。那么什么是 SPI 呢?
SPI,Service Provider Interface。即:接口服务的提供者。就是说我们应该面向接口(抽象)编程,而不是面向具体的实现来编程,这样一旦我们需要切换到当前接口的其他实现就无需修改代码。
在 Java 中,数据库驱动就使用到了 SPI 技术,每次我们只需要引入数据库驱动就能被加载的原因就是因为使用了 SPI 技术。
打开 DriverManager 类,其初始化驱动的代码如下:
进入 ServiceLoader 方法,发现其内部定义了一个变量:
private static final String PREFIX = "META-INF/services/";
这个变量在下面加载驱动的时候有用到,下图中的 service 即 java.sql.Driver:
所以就是说,在数据库驱动的 jar 包下面的 META-INF/services/ 下有一个文件 java.sql.Driver,里面记录了当前需要加载的驱动,我们打开这个文件可以看到里面记录的就是驱动的全限定类名:
@AutoConfigurationPackage 注解
从这个注解继续点进去之后可以发现,它最终还是一个 @Import 注解:
这个时候它导入了一个 AutoConfigurationPackages 的内部类 Registrar, 而这个类其实作用就是读取到我们在最外层的 @SpringBootApplication 注解中配置的扫描路径(没有配置则默认当前包下),然后把扫描路径下面的类都加到数组中返回。
SpringBoot Starter的出现
我们可以看下SpringBoot 现在都为我们提供有哪些starter,我这边这截图了部分starter,更多的请点击https://github.com/spring-projects/spring-boot/tree/master/spring-boot-project/spring-boot-starters
starter的实现:虽然我们每个组件的starter实现各有差异,但是它们基本上都会使用到两个相同的内容:ConfigurationProperties和AutoConfiguration。因为Spring Boot提倡“约定大于配置”这一理念,所以我们使用ConfigurationProperties来保存我们的配置,并且这些配置都可以有一个默认值,即在我们没有主动覆写原始配置的情况下,默认值就会生效。除此之外,starter的ConfigurationProperties还使得所有的配置属性被聚集到一个文件中(一般在resources目录下的application.properties),这样我们就告别了Spring项目中XML地狱。
starter的出现帮把我们把各种复杂的配置都封装起来了,让我们真正的可以达到了开箱即用。不仅降低了我们使用它的门槛,并且还大大提高了我们的开发效率。正如前面所说《SpringBoot自动装配》让我们有更多的时间去陪女朋友。
手写自定义一个starter
了解完自动装配的原理,接下来就可以动手写一个自己的 starter 组件了。
starter 组件命名规则
SpringBoot 官方的建议是,如果是我们开发者自己开发的 starter 组件(即属于第三方组件),那么命名规范是{name}-spring-boot-starter,而如果是 SpringBoot 官方自己开发的组件,则命名为 spring-boot-starter-{name}`。
当然,这只是一个建议,如果非不按这个规则也没什么问题,但是为了更好的识别区分,还是建议按照这个规则来命名。
手写自定义一个starter
下面我们就来实现一个自定义的发送短信的starter,命名为sms-spring-boot-starter
。
1、引入pom
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.18</version> <scope>provided</scope> </dependency>
2、编写配置文件
发短信我们需要配置一些账号信息,不同的短信供应商,账户信息是不一样的,所以我们需要定义一个XXXXProperties
来自动装配这些账户信息。下面我们就以腾讯云和阿里云两家供应商为例;
@ConfigurationProperties(prefix = "sms") @Data public class SmsProperties { private SmsMessage aliyun = new SmsMessage(); private SmsMessage tencent = new SmsMessage(); @Data public static class SmsMessage{ /** * 用户名 */ private String userName; /** * 密码 */ private String passWord; /** * 秘钥 */ private String sign; private String url; @Override public String toString() { return "SmsMessage{" + "userName='" + userName + '\'' + ", passWord='" + passWord + '\'' + ", sign='" + sign + '\'' + ", url='" + url + '\'' + '}'; } } }
如果需要在其他项目中使用发送短信功能的话,我们只需要在配置文件(application.yml
)中配置SmsProperties
的属性信息就可以了。 比如:
sms: aliyun: pass-word: 12345 user-name: java金融 sign: 阿里云 url: http://aliyun.com/send tencent: pass-word: 6666 user-name: java金融 sign: 腾讯云 url: http://tencent.com/send
还记的@ConfigurationProperties注解里面是不是有个prefix 属性,我们配置的这个属性是sms,配置这个的主要一个作用的话是主要用来区别各个组件的参数。这里有个小知识点需要注意下当我们在配置文件输入sms我们的idea会提示这个sms有哪些属性可以配置,以及每个属性的注释都有标记,建议的话注释还是写英文,这样会显得你比较专业。
这个提示的话,是需要引入下面这个jar
的。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
引入这个jar
之后,我们编译之后就会在META-INF
文件夹下面生成一个spring-configuration-metadata.json
的文件。
我们可以看到这个文件其实 是根据SmsProperties类的成员属性来生成的。
3、然后在编写短信自动配置类:
@EnableConfigurationProperties(value = SmsProperties.class) @Configuration public class SmsAutoConfiguration { /** * 阿里云发送短信的实现类 * @param smsProperties * @return */ @Bean public AliyunSmsSenderImpl aliYunSmsSender(SmsProperties smsProperties){ return new AliyunSmsSenderImpl(smsProperties.getAliyun()); } /** * 腾讯云发送短信的实现类 * @param smsProperties * @return */ @Bean public TencentSmsSenderImpl tencentSmsSender(SmsProperties smsProperties){ return new TencentSmsSenderImpl(smsProperties.getTencent()); } }
编写我们的发送短信实现类:
public class AliyunSmsSenderImpl implements SmsSender { private SmsMessage smsMessage; public AliyunSmsSenderImpl(SmsMessage smsProperties) { this.smsMessage = smsProperties; } @Override public boolean send(String message) { System.out.println(smsMessage.toString()+"开始发送短信==》短信内容:"+message); return true; } }
4、让starter生效
starter集成应用有两种方式:
被动生效
我们首先来看下我们熟悉的方式,通过SpringBoot的SPI的机制来去加载我们的starter。我们需要在META-INF下新建一个spring.factories文件key为org.springframework.boot.autoconfigure.EnableAutoConfiguration, value是我们的SmsAutoConfiguration 全限定名(记得去除前后的空格,否则会不生效)。
主动生效
在starter
组件集成到我们的Spring Boot
应用时需要主动声明启用该starter
才生效,通过自定义一个@Enable
注解然后在把自动配置类通过Import
注解引入进来。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({SmsAutoConfiguration.class}) public @interface EnableSms { }
使用的时候需要在启动类上面开启这个注解。
5、打包,部署到仓库
如果是本地的话,直接通过mvn install
命令就可以了。
如果需要部署到公司的仓库话,这个就不说了。
6、新建一个新的SpringBoot
项目引入我们刚写的starter
<dependency> <groupId>com.workit.sms</groupId> <artifactId>sms-spring-boot-starter</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
在项目配置文件配上短信账号信息
测试代码:
@SpringBootApplication @EnableSms public class AutoconfigApplication { public static void main(String[] args) { ConfigurableApplicationContext applicationContext = SpringApplication.run(AutoconfigApplication.class, args); AliyunSmsSenderImpl aliyunSmsSender = applicationContext.getBean(AliyunSmsSenderImpl.class); aliyunSmsSender.send("用阿里云发送短信"); TencentSmsSenderImpl tencentSmsSender = applicationContext.getBean(TencentSmsSenderImpl.class); tencentSmsSender.send("用腾讯云发送短信"); }
运行结果:
SmsMessage{userName='java金融', passWord='12345', sign='阿里云', url='http://aliyun.com/send'}开始发送短信==》短信内容:用阿里云发送短信
SmsMessage{userName='java金融', passWord='6666', sign='腾讯云', url='http://tencent.com/send'}开始发送短信==》短信内容:用腾讯云发送短信
至此的话我们自定义的一个starter
就已经完成了,这个starter
只是一个演示的demo
,代码有点粗糙,项目结构也有点问题。重点看下这个实现原理就好。赶紧动动小手去实现一个自己的starter
吧。
参考文章:
https://xie.infoq.cn/article/87b345da6c3ad1cc967d81ad3
https://blog.csdn.net/zengfanwei1990/article/details/107537251
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架