从 Spring 进入到 SpringBoot 有一个最直观的感受 —— 省去了大量 xml 的配置,一切看起来都是十分简洁高效的
什么是自动装配 现在提到自动装配都默认是 SpringBoot,其实 Spring 很早就实现了这个功能,SpringBoot 只是在其基础上,通过 SPI 的方式,做了进一步的优化
SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时 会扫描外部引用 jar 包中的 META-INF/spring.factories
文件,按照文件中配置的类信息加载到 Spring 容器中,并执行类中定义的各种操作。对于外部 jar 来说,只需按照 SpringBoot 定义的标准,就能将自己的功能装配进 SpringBoot
SpringBoot3.0 之后不再使用 SpringFactoriesLoader,而是 Spring 重新从 META-INFO/spring/
目录下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中读取,其本质还是不变的,只是文件路径和文件名的改变
如何自动装配 既然是启动时自动装配的,先看启动时的核心注解 @SpringBootApplication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication { @AliasFor( annotation = EnableAutoConfiguration.class ) Class<?>[] exclude() default {}; @AliasFor( annotation = EnableAutoConfiguration.class ) String[] excludeName() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackages" ) String[] scanBasePackages() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackageClasses" ) Class<?>[] scanBasePackageClasses() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "nameGenerator" ) Class<? extends BeanNameGenerator > nameGenerator() default BeanNameGenerator.class; @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods () default true ; }
@SpringBootApplication
由三个注解组成:@SpringBootConfiguration
、@EnableAutoConfiguration
和 @ComponentScan
,其中 @SpringBootConfiguration
相当于是 @Configuration
1 2 3 4 5 6 7 8 9 10 11 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration @Indexed public @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods () default true ; }
三个关键注解的作用:
@Configuration :允许上下文中注册额外的 Bean 或导入其他配置类
@EnableAutoConfiguration :启用 SpringBoot 的自动装配
@ComponentScan :指定包扫描路径(默认为启动类所有包下),可指定基础包以及排除某些类
@EnableAutoConfiguration 工作原理 @EnableAutoConfiguration
是靠 AutoConfigurationImportSelector
进行解析加载的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class AutoConfigurationImportSelector implements DeferredImportSelector , BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { private static final String[] NO_IMPORTS = new String [0 ]; public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!this .isEnabled(annotationMetadata)) { return NO_IMPORTS; } else { AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this .beanClassLoader); AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this .getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } } } public interface DeferredImportSelector extends ImportSelector {} public interface ImportSelector { String[] selectImports(AnnotationMetadata var1); }
AutoConfigurationImportSelector
实现了 ImportSelector
接口的 selectImports
方法,方法里的关键在于 getAutoConfigurationEntry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry ();AutoConfigurationEntry getAutoConfigurationEntry (AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { if (!this .isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { AnnotationAttributes attributes = this .getAttributes(annotationMetadata); List<String> configurations = this .getCandidateConfigurations(annotationMetadata, attributes); configurations = this .removeDuplicates(configurations); Set<String> exclusions = this .getExclusions(annotationMetadata, attributes); this .checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this .filter(configurations, autoConfigurationMetadata); this .fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector .AutoConfigurationEntry(configurations, exclusions); } }
与 SPI 不同的是,SpringBoot 并不会将所有的配置都装载进来,装载的策略会按条件进行过滤,过滤方法在第 21 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 private List<String> filter (List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) { long startTime = System.nanoTime(); String[] candidates = StringUtils.toStringArray(configurations); boolean [] skip = new boolean [candidates.length]; boolean skipped = false ; Iterator var6 = this .filters.iterator(); int i; while (var6.hasNext()) { AutoConfigurationImportFilter filter = (AutoConfigurationImportFilter)var6.next(); boolean [] match = filter.match(candidates, this .autoConfigurationMetadata); for (int i = 0 ; i < match.length; i++) { if (!match[i]) { skip[i] = true ; candidates[i] = null ; skipped = true ; } } } if (!skipped) { return configurations; } else { List<String> result = new ArrayList <>(candidates.length); for (int i = 0 ; i < candidates.length; i++) { if (!skip[i]) { result.add(candidates[i]); } } if (logger.isTraceEnabled()) { int numberFiltered = configurations.size() - result.size(); logger.trace("Filtered " + numberFiltered + " auto configuration class in " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms" ); } return result; } }
这里的 filter
方法主要就是 通过 AutoConfigurationImportFilter
接口的 match
方法来判断每个自动配置类上面的条件注解(@ConditionOnClass
、@ConditionOnBean
和 @ConditionOnWebApplication
)是否满足
整体流程 SpringBoot 自动装配的整体流程:
利用 SPI 机制,从配置文件中读取需要自动配置的类
过滤 @EnableAutoConfiguration
中 exclude
属性所要排除的类
再判断 @ConditionOnClass
、@ConditionOnBean
和 @ConditionOnWebApplication
这三个条件注解是否满足条件
然后触发AutoConfigurationImportEvent
事件,告诉ConditionEvaluationReport
条件评估报告器对象来分别记录符合条件和exclude
的自动配置类
最后将筛选后的自动配置类导入 IOC 容器中
创建一个自己的自动装配 Starter
基于 SpringBoot 3.X
模块 如果项目具有多种风格、选项或者可选特性,通常会定义两个模块 —— autoconfigure
和 starter
如果自动配置相对简单,没有可选特性,那么可以合并为一个 starter
命名 自动配置模块通常命名为:xxx-spring-boot-autoconfigure
starter 模块通常命名为:xxx-spring-boot-starter
配置参数 项目自定义的一些配置参数为确保命名空间的唯一,通常需要以项目名为开头
1 2 3 4 @ConfigurationProperties("xxx") public class XxxProperties { private String name = "xxx" ; }
SpringBoot 内部针对配置的相关规则:
不要使用「The」或者「A」开头
对于布尔类型,用「Whether」或者「Enable」
对于集合类型,使用逗号分隔的列表
使用 java.time.Duration
而不是 long
,并描述与毫秒不同的默认单位
为确保配置的正确生成,需要检查生成的元数据 META-INF/spring-configuration-metadata.json
autoconfigure模块包含starter模块库所需的所有内容。它还可能包含配置键定义(例如@ConfigurationProperties)和任何回调接口,这些接口可以用于进一步定制组件的初始化方式
SpringBoot 使用一个注释处理器来收集元数据文件中自动配置的条件( META-INF/spring-autoconfiguration-metadata.properties
)。如果该文件存在,它将用于主动过滤不匹配的自动配置,这将提高启动时间。因此建议在包含自动配置的模块中添加以下依赖项:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-autoconfigure-processor</artifactId > <optional > true</optional > </dependency >
如果在应用程序中直接定义了自动配置,确保配置了 spring-boot-maven-plugin
,以防止重新打包目标将依赖项添加到 fat.jar 中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <project > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <configuration > <excludes > <exclude > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-autoconfigure-processor</artifactId > </exclude > </excludes > </configuration > </plugin > </plugins > </build > </project >
starter 模块 starter 模块其实是一个空的 jar,它的唯一目的是提供使用库所需的依赖项
示例
开发一个模拟发送邮件的自定义 starter
,最终的效果任何其它的项目只需引入 starter
配置基本的邮件配置,就可以使用 SendMailService
发送邮件。项目的结构 autoconfigure
和 starter
模块分开
1. 新建项目 email-spring-boot 设置顶级 pom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <groupId > net.venom24</groupId > <artifactId > email-spring-boot</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > email-spring-boot</name > <description > 模拟 email 发送 starter</description > <modules > <module > email-spring-boot-autoconfigure</module > <module > email-spring-boot-starter</module > </modules > <packaging > pom</packaging > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > 3.0.0-M2</version > <type > pom</type > <scope > import</scope > </dependency > </dependencies > </dependencyManagement >
添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <parent > <groupId > net.venom24</groupId > <artifactId > email-spring-boot</artifactId > <version > 0.0.1-SNAPSHOT</version > </parent > <artifactId > email-spring-boot-autoconfigure</artifactId > <packaging > jar</packaging > <description > autoconfigure </description > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-autoconfigure</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <configuration > <excludes > <exclude > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-autoconfigure-processor</artifactId > </exclude > </excludes > </configuration > </plugin > </plugins > </build >
添加配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Getter @Setter @ConfigurationProperties("email.service") public class EmailProperties { private boolean enable=true ; private String host; private Integer port; private String name; private String password; }
添加模拟邮件发送功能 EmailService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class EmailService { private EmailProperties mailProperties; public EmailService (EmailProperties mailProperties) { this .mailProperties = mailProperties; } public void send (String content) { System.out.println("开始发送邮件:" ); String info = "host:%s,port:%s" ; System.out.println(String.format(info, mailProperties.getHost(), mailProperties.getPort())); System.out.println("发送内容:" + content); System.out.println("发送成功!" ); } }
添加自动配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration @ConditionalOnClass(EmailService.class) @EnableConfigurationProperties(value = EmailProperties.class) public class EmailAutoConfiguration { private final EmailProperties mailProperties; public EmailAutoConfiguration (EmailProperties mailProperties) { this .mailProperties = mailProperties; } @Bean @ConditionalOnMissingBean(EmailService.class) @ConditionalOnProperty(prefix = "email.service", value = "enable", havingValue = "true") public EmailService mailService (EmailProperties mailProperties) { return new EmailService (mailProperties); } }
简单解释下各个注解:
@Configuration :声明配置类
**@ConditionalOnClass(EmailService.class)**:只有在 classpath
中找到 EmailService
类的情况下才会解析这个自动配置类
**@EnableConfigurationProperties(value = EmailProperties.class)**:启用自动装配
@Bean :实例化一个 Bean
**@ConditionalOnMissingBean(EmailService.class)**:与 @Bean
配合使用,只有当 Spring 上下文中不存在 EmailService 时才会执行该实例化方法
**@ConditionalOnProperty(prefix = “email.service”, value = “enable”, havingValue = “true”)**: 当 classpath 中存在 EmailService 类时解析此配置类,并且在 Spring 上下文中没有 EmailService 的 bean 实例的情况下,new 一个实例出来,然后将应用配置中的相关配置值传入
添加自动装配文件
META-INF/spring/
下新建 org.springframework.boot.autoconfigure.AutoConfiguration.imports
1 net.venom24.email.config.EmailAutoConfiguration
3. 新建模块 email-spring-boot-starter 添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <parent > <groupId > net.venom24</groupId > <artifactId > email-spring-boot</artifactId > <version > 0.0.1-SNAPSHOT</version > </parent > <artifactId > email-spring-boot-starter</artifactId > <packaging > jar</packaging > <description > starter </description > <dependencies > <dependency > <groupId > net.venom24</groupId > <artifactId > email-spring-boot-autoconfigure</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > </dependencies >
email-spring-boot-starter
模块主要是集成 email-spring-boot-autoconfigure
,以及一些其它的依赖项
4. 测试 新建一个测试项目,引入 email-spring-boot-starter
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.example</groupId > <artifactId > email-spring-boot-starter</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency >
配置参数
1 2 3 4 5 email.service.enable =true email.service.host =mail.qq.com email.service.port =123 email.service.name =admin email.service.password =admin
新建测试类测试
1 2 3 4 5 6 7 8 9 @SpringBootTest class EmailStarterTestApplicationTests { @Autowired private EmailService emailService; @Test void contextLoads () { emailService.send("我是自定义starter发送的邮件" ); } }
Output:
1 2 3 4 开始发送邮件 host:mail.qq.com,port:123 发送内容:我是自定义starter发送的邮件 发送成功!
附 SpringBoot 提供的条件注解
@ConditionalOnBean :当容器里有指定 Bean 的条件下
@ConditionalOnMissingBean :当容器里没有指定 Bean 的情况下
@ConditionalOnSingleCandidate :当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
@ConditionalOnClass :当类路径下有指定类的条件下
@ConditionalOnMissingClass :当类路径下没有指定类的条件下
@ConditionalOnProperty :指定的属性是否有指定的值
@ConditionalOnResource :类路径是否有指定的值
@ConditionalOnExpression :基于 SpEL 表达式作为判断条件
@ConditionalOnJava :基于 Java 版本作为判断条件
@ConditionalOnJndi :在 JNDI 存在的条件下差在指定的位置
@ConditionalOnNotWebApplication :当前项目不是 Web 项目的条件下
@ConditionalOnWebApplication :当前项目是 Web 项 目的条件下