一、引言
使用过SpringBoot的应该知道,SpringBoot通过默认配置很多框架的使用方式,帮我们大大简化了项目初始搭建以及开发过程。
下面将一步步分析SpringBoot的启动过程,着重分析SpringBoo的特性,自动装配。
首先回想一下用Spring框架是如何搭建Web项目的,通常要搭建一个基于Spring的Web应用,我们需要做以下一些工作:
- pom文件中引入相关jar包,包括spring、springmvc、redis、mybaits、log4j等相关jar包
- 配置web.xml,加入Listener配置、Filter配置、Servlet配置、log4j配置、error配置等
- 配置数据库连接、配置spring事务
- 配置视图解析器
- 开启注解、自动扫描功能
- 配置完成后部署tomcat、启动调试
- ……
对于熟悉这些流程的人来说,搭建一个初始项目,可能一个小时或者半天就过去了,不熟的可能就……但是用了SpringBoot之后一切都会变得非常便捷,下面首先来分析一下SpringBoot的起步依赖以及自动配置。
二、起步依赖
1、pom文件
在SpringBoot项目中,pom文件里面会引入以下jar:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--springboot web模块支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis 开发包 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
spring-boot-starter-web包自动帮我们引入了web模块开发需要的相关jar包
mybatis-spring-boot-starter包自动帮我们引入了dao开发相关的jar包
注:像spring-boot-starter-xxx
这样的包是官方提供的starter,而像xxx-spring-boot-starter
这样的包是第三方提供的starter
下面看一下mybatis-spring-boot-starter依赖
可以看出mybatis-spring-boot-starter
并没有任何源码,只有一个pom文件,它的作用就是帮我们引入其它jar
2、配置数据源
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://127.0.0.1:3306/mybatis_test
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
hikari:
# 最小空闲连接数量
minimum-idle: 5
# 连接池最大连接数,默认是10
maximum-pool-size: 60
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 一个连接idle状态的最大时长(毫秒),超时则被释放(retired),缺省:10分钟
idle-timeout: 600000
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 60000
stater机制帮我们完成了项目起步所需要的的相关jar包。那问题来了,在传统的Spring应用中,我们要在application.xml
中配置很多Bean,比如:dataSource的配置,transactionManager的配置……那SpringBoot是如何帮我们完成这些Bean的配置的呢?下面我们来分析这个过程……
三、自动配置
1、基于java代码的Bean配置
以mybatis为例,观察mybatis-spring-boot-starter
这个包的依赖关系,可以发现mybatis-spring-boot-starter
这个包帮我们自动引入了mybatis-spring-boot-autoconfigure
这个包,这个包是干嘛呢?顾名思义,是起自动配置作用的,如下图:
里面有MybatisAutoConfiguration
这个类,打开这个类看看有些什么东西。
熟悉@Configuration、@Bean这两个注解的人或许已经知道了,这两个注解一起使用就可以创建一个基于java代码的配置类,可以用来替代相应的xml配置文件。
被@Configuration注解修饰的类,可以看作是能生产让Spring IoC容器管理的Bean实例的工厂
被@Bean注解修饰的方法,是告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册到spring容器中
所以上面的MybatisAutoConfiguration
这个类,自动帮我们生成了SqlSessionFactory
这些Mybatis的重要实例并交给Spring容器管理,从而完成Bean的自动注册
2、自动配置条件依赖
从MybatisAutoConfiguration
这个类中使用的注解可以看出,要完成自动配置是有依赖条件的
@Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
private final Interceptor[] interceptors;
private final ResourceLoader resourceLoader;
private final DatabaseIdProvider databaseIdProvider;
private final List<ConfigurationCustomizer> configurationCustomizers;
......
首先看一下SpringBoot常用的条件依赖注解:
- @ConditionalOnBean:仅在当前上下文中存在某个bean时,才会实例化这个Bean
- @ConditionalOnClass:在类路径上,能找到某个class,才会实例化这个Bean
- @ConditionalOnExpression:当表达式为true的时候,才会实例化这个Bean
- @ConditionalOnMissingBean,仅在当前上下文中不存在某个bean时,才会实例化这个Bean
- @ConditionalOnMissingClass,某个class在类路径上不存在的时候,才会实例化这个Bean
- @ConditionalOnNotWebApplication:不是web应用时才会实例化这个Bean
- @AutoConfigureAfter,在某个bean完成自动配置后实例化这个bean
- @AutoConfigureBefore,在某个bean完成自动配置前实例化这个bean
根据这些依赖条件,所以要完成Mybatis的自动配置,需要在类路径中存在SqlSessionFactory.class
、SqlSessionFactoryBean.class
这两个类,在spring容器中,需要存在DataSource这个bean且这个bean完成自动注册。
再看下这个注解条件:
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
这个注解的意思是:在DataSourceAutoConfiguration
这个类完成自动配置后,再去实例化这个MybatisAutoConfiguration
,进入DataSourceAutoConfiguration
这个类,可以看到这个类属于这个包:
org.springframework.boot.autoconfigure.jdbc
这个包又属于spring-boot-autoconfigure-2.0.4.RELEASE.jar
这个包,这个自动配置包帮我们引入了jdbc、kafka、logging、mail、mongo等包。很多包需要我们引入相应jar后自动配置才生效。
3、Bean参数的获取
至此,我们已经知道了Bean的配置过程,但是还没有看到SpringBoot是如何读取yml或者properites配置文件的属性来创建数据源的?
在DataSourceAutoConfiguration
类里面,我们注意到使用了EnableConfigurationProperties
这个注解。
这个@EnableConfigurationProperties注解是干嘛的,可以参考另一篇文章:
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
......
进入DataSourceProperties
中,可以看到,它封装了数据源的各个属性,且使用了注解ConfigurationProperties
指定了配置文件的前缀。
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
// Name of the datasource. Default to "testdb" when using an embedded database.
private String name;
// Whether to generate a random datasource name.
private boolean generateUniqueName;
/**
* Fully qualified name of the connection pool implementation to use. By default, it
* is auto-detected from the classpath.
*/
private Class<? extends DataSource> type;
// Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
private String driverClassName;
// JDBC URL of the database.
private String url;
// Login username of the database.
private String username;
// Login password of the database.
private String password;
// JNDI location of the datasource. Class, url, username & password are ignored when set.
private String jndiName;
// Initialize the datasource with available DDL and DML scripts.
private DataSourceInitializationMode initializationMode =
DataSourceInitializationMode.EMBEDDED;
/**
* Platform to use in the DDL or DML scripts (such as schema-${platform}.sql or
* data-${platform}.sql).
*/
private String platform = "all";
// Schema (DDL) script resource references.
private List<String> schema;
// Username of the database to execute DDL scripts (if different).
private String schemaUsername;
// Password of the database to execute DDL scripts (if different).
private String schemaPassword;
// Data (DML) script resource references.
private List<String> data;
......
下面简单说一下几个注解作用:
@ConfigurationProperties注解的作用:是把yml或者properties配置文件转化为Bean
@EnableConfigurationProperties注解的作用:是使@ConfigurationProperties注解生效。
如果只配置@ConfigurationProperties注解,在spring容器中是获取不到yml或者properties配置文件转化的bean的
通过这种方式,把yml或者properties配置参数转化为Bean,这些Bean又是如何被发现与加载的?
4、Bean的发现
SpringBoot默认扫描启动类所在的包下的主类与子类的所有组件,但并没有包括依赖包的中的类,那么依赖包中的Bean是如何被发现和加载的?
我们通常在启动类上加@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 {
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
......
实际上重要的只有三个注解:
- @Configuration(@SpringBootConfiguration里面还是应用了@Configuration)
- @EnableAutoConfiguration
- @ComponentScan
@Configuration的作用上面我们已经知道了,被注解的类将成为一个bean配置类。
@ComponentScan的作用就是自动扫描并加载符合条件的组件,比如@Component和@Repository等,最终将这些Bean定义加载到Spring容器中。
@EnableAutoConfiguration 这个注解的功能很重要,借助@Import的支持,收集和注册依赖包中相关的bean定义。
对于@Import注解的使用,可以参考另一篇文章:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
如上源码,@EnableAutoConfiguration
注解引入了@AutoConfigurationPackage
和@Import
这两个注解。
@AutoConfigurationPackage
的作用就是自动配置的包,@Impor
t导入需要自动配置的组件。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
那问题又来了,要搜集并注册到Spring容器的那些Beans来自哪里?
进入AutoConfigurationImportSelector
类,我们可以发现``SpringFactoriesLoader.loadFactoryNames方法调用
loadSpringFactories`方法从所有的jar包中读取META-INF/spring.factories文件信息。
下面是spring-boot-autoconfigure
这个jar中spring.factories
文件部分内容,其中有一个key为org.springframework.boot.autoconfigure.EnableAutoConfiguration
的值定义了需要自动配置的Bean,通过读取这个配置获取一组@Configuration类。
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
每个xxxAutoConfiguration都是一个基于java的bean配置类。实际上,这些xxxAutoConfiguration不是所有都会被加载,会根据xxxAutoConfiguration上的@ConditionalOnClass等条件判断是否加载,通过反射机制将spring.factories中@Configuration类实例化为对应的java实列。
到此我们已经知道怎么发现要自动配置的Bean了,最后一步就是怎么样将这些Bean加载到spring容器。
5、Bean 加载
如果要让一个普通类交给Spring容器管理,通常有以下方法:
- 使用 @Configuration与@Bean 注解
- 使用@Controller、@Service、@Repository、@Component 注解标注该类,然后启用@ComponentScan自动扫描
- 使用@Import方法
SpringBoot中使用了@Import 方法,`@EnableAutoConfiguration`注解中使用了`@Import({AutoConfigurationImportSelector.class})`注解。
AutoConfigurationImportSelector
实现了DeferredImportSelector
接口,DeferredImportSelector
接口继承了ImportSelector
接口,ImportSelector
接口只有一个selectImports方法。
selectImports方法返回一组Bean,
@EnableAutoConfiguration
注解借助@Import注解将这组Bean注入到Spring容器中,SpringBoot正式通过这种机制来完成Bean的注入的
四、总结
我们可以将自动配置的关键几步以及相应的注解总结如下:
- @Configuration与@Bean------>基于java代码的bean配置
- @Conditional------>设置自动配置条件依赖
- @EnableConfigurationProperties与@ConfigurationProperties------>读取配置文件转换为bean。
- @EnableAutoConfiguration、@AutoConfigurationPackage 与@Import------>实现bean发现与加载。