在前两篇我们介绍了 SpringBoot 的基本使用
那现在来思考一个问题,SpringBoot是如何实现自动装配的呢?
自动装配:
- 相对非自动而言,在Spring中当要使用某个外部组件时,必须通过配置或JavaConfig将其手动显示注册
- 在SpringBoot中,这些外部依赖组件,在引入 相应Stater后就做到了开箱即用,基本0配置(除必要属性)
为了揭开 springboot 的奥秘,我们直接从 Annotation 入手,看看@SpringBootApplication里面,做了什么?
@SpringBootApplication
打开SpringBootApplication这个注解,可以看到它实际上是一个复合注解
@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 {};
// 直接根据class类扫描
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
可以看出核心是以下三个注解:
- @SpringBootConfiguration – 标识当前类是配置类
- @EnableAutoConfiguration – 通过@Import + ImportSelector 动态导入所有 bean 配置
- @ComponetScan – 扫描所有标有 @Controller、@Service、@Componet 等注解的 bean
我们可以直接用这三个注解启动 springboot 应用,因为所有要注册的 bean = @Configuration + @ComponentScan。只是每次配置三个注解比较繁琐,所以直接用一个复合注解更方便些。
1.@SpringBootConfiguration
点打开 @SpringBootConfiguration 注解,我们可以看到它的关键是 @Configuration
@Configuration 这个注解大家应该有用过,它是JavaConfig形式的基于Spring IOC容器的配置类使用的一种注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 本质也是一个Bean,需要被扫描发现
public @interface Configuration {
@AliasFor(annotation = Component.class)
String value() default "";
}
传统的xml形式配置Bean:
<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/test" />
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<beans>
JavaConfig 配置Bean:
Java5 引入了 Annotations 这个特性,Spring 框架也紧随大流并且推出了基于 Java 代码和 Annotation 元信息的依赖关系绑定描述的方式。也就是JavaConfig。从 spring3 开始 spring 就支持了两种bean的配置方式。
@Configuration // @Configuration(配置类)就相当于一个beans.xml
public class config {
@Bean // 等价于 <bean>
// 返回值 = bean的class ; 方法名 = bean的id
public BasicDataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(); // 这些设置 = property
...
return dataSource;
}
}
注:@Configuration本质上也是一个Bean,也需要被扫描
2.@EnableAutoConfiguration
@EnableAutoConfiguration 的主要作用其实就是把所有符合条件的@Configuration配置(实质上是要注册的beans)都加载到当前SpringBoot创建并使用的IOC容器中。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
// 按类型排除不需要自动装配的类
Class<?>[] exclude() default {};
// 按名称排除不需要自动装配的类
String[] excludeName() default {};
}
在 spring3.1 版本中,提供了一系列的@Enable 开头的注解,Enable主机应该是在JavaConfig框架上更进一 步的完善,使得用户在使用spring相关的框架时,避免配置大量的代码,从而降低使用的难度。比如常见的一些 Enable 注解:
- @EnableWebMvc,引入 MVC 框架在 Spring 应用中需要用到的所有 bean
- @EnableScheduling,开启计划任务的支持;
找到 EnableAutoConfiguration,我们可以看到每一个涉及到Enable开头的注解,都会带有一个@Import的注解。
2.1 @Import
@Import 就是把多个分散的容器配置合并在一个配置中,类似于 xml 形式下有一个 <import resource/>
形式的注解。
注意,@Import导入是类名.class,可以导入任何类,不要求必须标@Componet交给SpringIOC)
问题一:@Import如何使用?
@Import 它有两种导入 bean 的方式
- 静态导入:直接指定要注入的 bean
- 普通Java类
- @Configuration 配置类
- 动态导入:运行时再决定注入的 bean,有点点反射的味道
- 实现 ImportSelector 接口
- 实现 ImportBeanDefinitionRegistrar 接口
问题二:这里为什么需要 @Import?
每个@Configuration都独立的,其本质上也是一个Bean(需要被扫描),而Import导入就相当于扫描进来了。
比如一个 config 中的 bean 构造时需要依赖另一个 config 中配置的 bean,就要把另一个 config 给 import 进来。而此处EnableAutoConfiguration虽然有@Configuration但其实没有具体的配置,所以需要@Import把具体配置引进来。
这里注意一点,@ComponentScan 也可以把相应配置扫进来,但一般不用它扫配置类。
- 普通Bean:如@Controller,@Component 等常用 @ComponentScan
- 配置类:常用@Import合并到一个配置中,然后再@CompentScan扫描一处即可
- 扩展配置类:如引入的依赖(mybatis,自定义组件等),一般在spring.factories文件中写明要加载的配置类
再回到 EnableAutoConfiguration 这个注解中,我们发现它的 import 是这样
@Import(AutoConfigurationImportSelector.class)
2.2 AutoConfigurationImportSelector
基于 ImportSelector 实现动态装载(根据具体环境决定注册哪些bean),继承关系如下:
public class AutoConfigurationImportSelector
implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
@Override
// Spring会把这个方法返回的类的全限定名数组里的所有的类都注入到 IOC 容器中
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断当前系统是否禁用了自动装配的功能
if (!isEnabled(annotationMetadata)) {
// 如果当前系统禁用了自动装配的功能,则会返回空数组(new String[0]),后续也就无法注入bean了
return NO_IMPORTS;
}
// 1.加载配置条件信息
// META-INF/spring-autoconfigure-metadata.properties文件
AutoConfigurationMetadata autoConfigurationMetadata =
AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
// 2.获取注解的属性及其值(PS:注解指的是@EnableAutoConfiguration注解)
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 3.根据以上条件获取要装载beanNames,返回一个List<String>(已经被过滤:避免没依赖的bean加载)
// 在classpath下所有的 META-INF/spring.factories 文件中查找
// key = org.springframework.boot.autoconfigure.EnableAutoConfiguration
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 4.对上一步返回的List中的元素去重、排序
configurations = removeDuplicates(configurations);
// 5.依据第2步中获取获取@EnableAutoConfiguration注解上的exclude、excludeName属性,排除一些类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
// 6.返回真正要装载的Beans
return StringUtils.toStringArray(configurations);
}
}
本质上来说,其实 EnableAutoConfiguration 会帮助 springboot 应用把所有符合 @Configuration 配置都加载到当前 SpringBoot 创建的IoC容器,而动态注入借助了 Spring 框架提供的一个工具类 SpringFactoriesLoader 的支持。以及用到了 Spring 提供的条件注解 @Conditional,选择性的针对需要加载的 bean 进行条件过滤。
问题一:SpringFactoriesLoader 是什么?
SpringFactoriesLoader这个工具类的使用。它其实和 java 中的 SPI 机制的原理是一样的,不过它比SPI更好的点在于不会一次性加载所有的类,而是根据 key 进行加载(一般是相应注解全类名)。
1).META-INF/spring.factories有很多个,因为Spring启动要有很多Bean启动。
在 SpringBoot 中我们使用每个外部的框架或者组件时,看似只引入了一个 starter,但实际上它最少包含了三个jar包:
- 框架/组件本身的 jar 包,比如 mybatis.jar
- 框架/组件和Spring集成的 jar 包,因为Spring要对它们的使用再进行封装,比如 mybatis-spring.jar
- 框架/组件在Spring中自动装配的 jar 包,比如 mybatis-spring-boot-autoconfigure.jar
所以我们就不难理解为什么类路径下会存在这么多的 spring.factories,因为会有许多的 bean 自动装配好去给用户使用
![](https://i-blog.csdnimg.cn/blog_migrate/6be39e6fcc1d644d9e315817ec242d6d.png)
关于自定义 starter 可以看我的下篇文章 【SpringBoot】手写 starter及自定义配置参数 …
2). 每个 spring.factories 里面都有一些 key,自动装配的 key 是 org.springframework.boot.autoconfigure.EnableAutoConfiguration
上面的 mybatis-spring-boot-autoconfiguration 只有自动装配这一个key,再来看一个有多个 key 的 spring.factories
问题二:这些~Configuration到底写的是什么呢?
我们打开上面的 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 来看看
可以看到,它就是一个提前写好的配置类而已,等到到时候 SpringBoot 启动时,会自动将它装载进去,从而实例化 SqlSessionFactory 和 SqlSessionTemplate。
但是这里还有个问题,类和方法上标的那些 @ConditionOnClass、@ConditionOnBean,@ConditionalOnMissingBean 干嘛的呀?是用来做条件过滤的。
问题三:上面说的条件过滤又是什么?
在分析 AutoConfigurationImportSelector 的源码时,我们看到它会先扫描 spring-autoconfiguration-metadata.properties 文件,最后再扫描spring.factories 对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢?
因为@Configuration其实是依托于其他的框架来加载的, 如果当前的classpath环境下没有相关联的依赖,则意味着这些类没必要进行加载。比如现在导入上面 RabbitAutoConfiguration 来对 RabbitMQ 的 bean 进行自动装配,但是我项目里根本就没有用到它,连他的依赖都没有,那加载他有什么意义。
所以,通过这种条件过滤可以 有效的减少@configuration类的数量从而降低 SpringBoot的启动时间。
Conditions | 描述 |
---|---|
@ConditionalOnBean | 在存在某个 bean 的时候 |
@ConditionalOnMissingBean | 不存在某个 bean 的时候 |
@ConditionalOnClass | 当前 classpath 可以找到某个类型的类时 |
@ConditionalOnMissingClass | 当前 classpath 不可以找到某个类型的类 时 |
@ConditionalOnResource | 当前 classpath 是否存在某个资源文件 |
@ConditionalOnProperty | 当前 jvm 是否包含某个系统属性为某个值 |
@ConditionalOnWebApplication | 当前 spring context 是否是 web 应用程序 |
![](https://i-blog.csdnimg.cn/blog_migrate/3864ea4d45be1f34538dd91d4dffab89.png)
3.@ComponentScan
ComponentScan 这个注解是大家接触得最多的了,相当 于 xml 配置文件中的<context:component-scan>
。
<!-- 扫描注解(除了controller外),将 controller 交给 SpringMVC 容器去管理 -->
<context:component-scan base-package="com.xupt.yzh">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
ComponentScan 做的事情就是告诉 Spring 从哪里找到 bean。所以它会扫描指定路径下标识了需要装配的类,自动装配到spring的Ioc容器中。 标识需要装配的类的形式主要是:@Component、 @Repository、@Service、@Controller 这类的注解标识的类。
ComponentScan 默认会扫描当前 package 下的的所有加了相关注解标识的类到IoC容器中。
所以,我们必须把启动类放置到代码的根目录下才能保证所有的 bean 都被扫描到!
问题:我好像还听说过个 @MapperScan 它和 @CompentScan 什么关系呢?
- @ComponentScan
- 此注解是用来扫描需要加入 IOC 容器的实体类,即是关系项目中类的依赖关系(注意:此注解并不创建类的实例)
- 默认情况下此注解扫扫描本工程下所有的包,但是如果使用了 basePackages 属性时,此注解就不会使用默认的扫描路径了
- @MapperScan:
- 此注解是扫描指定要变成实现类的接口所在包(是属于 mybatis 的注解)
- 包下面的所有接口,在编译之后 MyBatis 会手动为所有接口创建 BeanDefinition 并注册到 IOC 容器
所以使用 mybatis 时还需在 Spring 单独配置一个 MapperScannerConfigurer 去扫描所有 @Mapper 标识的接口
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 扫描所有dao接口的实现 -->
<property name="basePackage" value="com.xupt.yzh.dao"></property>
</bean>
但是在 SpringBoot 中就不用再去单独配了,因为在自动装配的 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 中配置了一个静态内部类 AutoConfiguredMapperScannerRegistrar 去扫描 @Mapper 的接口,然后去注册 BeanDefinition