1.前言
不论在工作中,亦或是求职面试,Spring Boot已经成为我们必知必会的技能项。除了某些老旧的政府项目或金融项目持有观望态度外,如今的各行各业都在飞速的拥抱这个已经不是很新的Spring启动框架。
当然,作为Spring Boot的精髓,自动配置原理的工作过程往往只有在“面试”的时候才能用得上,但是如果在工作中你能够深入的理解Spring Boot的自动配置原理,将无往不利。
Spring Boot的出现,得益于“习惯优于配置
”的理念,没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成),这是基于Spring 4.x提供的按条件配置Bean的能力。
2.SpringBoot的入口
我们开发任何一个Spring Boot项目,都会用到如下的启动类
@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[] args){
SpringApplication.run(Application.class, args);}}
从上面代码可以看出,Annotation定义(@SpringBootApplication
)和类定义(SpringApplication.run
)最为耀眼,所以要揭开SpringBoot的神秘面纱,我们要从这两位开始就可以了。
3.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 @interfaceSpringBootApplication{...}
虽然定义使用了多个Annotation进行了原信息标注,但实际上重要的只有三个Annotation:
@Configuration(@SpringBootConfiguration点开查看发现里面还是应用了@Configuration)@EnableAutoConfiguration@ComponentScan
所以,如果我们使用如下的SpringBoot启动类,整个SpringBoot应用依然可以与之前的启动类功能对等:
@Configuration@EnableAutoConfiguration@ComponentScanpublicclassApplication{publicstaticvoidmain(String[] args){
SpringApplication.run(Application.class, args);}}
每次写这3个比较累,所以写一个@SpringBootApplication
方便点。接下来分别介绍这3个Annotation。
4.@Configuration
这里的@Configuration
对我们来说不陌生,它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration
,SpringBoot社区推荐使用基于JavaConfig的配置形式,所以,这里的启动类标注了@Configuration之后,本身其实也是一个IoC容器的配置类。
举几个简单例子回顾下,XML跟config配置方式的区别:
表达形式层面
基于XML配置的方式是这样:
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"default-lazy-init="true"><!--bean定义--></beans>
而基于JavaConfig的配置方式是这样:
@ConfigurationpublicclassMockConfiguration{//bean定义}
任何一个标注了@Configuration
的Java类定义都是一个JavaConfig配置类。
注册bean定义层面
基于XML的配置形式是这样:
<beanid="mockService"class="..MockServiceImpl">
...</bean>
而基于JavaConfig的配置形式是这样的:
@ConfigurationpublicclassMockConfiguration{@Beanpublic MockServicemockService(){returnnewMockServiceImpl();}}
任何一个标注了@Bean
的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。
表达依赖注入关系层面
为了表达bean与bean之间的依赖关系,在XML形式中一般是这样:
<beanid="mockService"class="..MockServiceImpl">
<propery name ="dependencyService" ref="dependencyService" /></bean><beanid="dependencyService"class="DependencyServiceImpl"></bean>
而基于JavaConfig的配置形式是这样的:
@ConfigurationpublicclassMockConfiguration{@Beanpublic MockServicemockService(){returnnewMockServiceImpl(dependencyService());}@Beanpublic DependencyServicedependencyService(){returnnewDependencyServiceImpl();}}
如果一个bean的定义依赖其他bean,则直接调用对应的JavaConfig类中依赖bean的创建方法就可以了。
5.@ComponentScan扫描bean
我们原来使用spring的使用不会在xml中一个一个配置bean,我们在再类上加上@Repository
,@Service
,@Controller
,@Component
,并且注入时可以使用@AutoWired的注解注入。 这一切的功能都需要我们配置包扫描<context:component-scan base-package="com.bruceliu"/>.
然而现在注解驱动开发已经没有了配置文件,不能配置。但是提供了@ComponentScan,我们可以在配置类上面加上这个注解也是一样,并且也能扫描配置包项目的相关注解,也能完成自动注入。
接下来我们先来看扫描组件,后面再看注入
package com.bruceliu.service;import com.bruceliu.bean.User;import org.springframework.stereotype.Service;@ServicepublicclassUserService{public UsergetUser(Long id){
System.out.println("userservice...");return null;}}
package com.bruceliu.controller;import com.bruceliu.bean.User;import org.springframework.stereotype.Controller;@ControllerpublicclassUserController{//先不拷贝页面,直接打印即可public UsergetUser(Long id){
System.out.println("usercontroller...");return null;}}
//注解类==配置文件@Configuration//告诉spring这是一个注解类@ComponentScan("com.bruceliu")publicclassMainConfig{//相当于在xml中配置了<bean id="" class="com.bruceliu.dao.UserDao"><bean/>@Bean("userDao")//指定bean的名字public UserDaouserDao01(){returnnewUserDaoImpl();}}
publicclassMainConfigTest{@TestpublicvoidtestIoc(){
ApplicationContext context=newAnnotationConfigApplicationContext(MainConfig.class);for(String beanName: context.getBeanDefinitionNames()){
System.out.println(beanName);}}}
注:所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages。
6.@EnableAutoConfiguration
@EnableAutoConfiguration
作为一个复合Annotation,其自身定义关键信息如下:
@SuppressWarnings("deprecation")@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(EnableAutoConfigurationImportSelector.class)public @interfaceEnableAutoConfiguration{...}
其中,最关键的要属@Import(EnableAutoConfigurationImportSelector.class)
,借助EnableAutoConfigurationImportSelector
,@EnableAutoConfiguration
可以帮助SpringBoot应用将所有符合条件的@Configuration
配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样借助于Spring框架原有的一个工具类:SpringFactoriesLoader
的支持,@EnableAutoConfiguration
可以智能的自动配置功效才得以大功告成!
而这个注解也是一个派生注解,其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()
方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories
的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。
这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔,如下图所示:
这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(…)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。
7.自动配置生效
每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,常见的条件注解有如下几项:
@ConditionalOnBean:当容器里有指定的bean的条件下。
@ConditionalOnMissingBean:当容器里不存在指定bean的条件下。
@ConditionalOnClass:当类路径下有指定类的条件下。
@ConditionalOnMissingClass:当类路径下不存在指定类的条件下。
以ServletWebServerFactoryAutoConfiguration
配置类为例,解释一下全局配置文件中的属性如何生效,比如:server.port=8081
,是如何生效的(当然不配置也会有默认值,这个默认值来自于org.apache.catalina.startup.Tomcat)。
在ServletWebServerFactoryAutoConfiguration
类上,有一个@EnableConfigurationProperties
注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。
在这个类上,我们看到了一个非常熟悉的注解:@ConfigurationProperties
,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties
负责导入这个已经绑定了属性的bean到spring容器中(见上面截图)。那么所有其他的和这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。
至此,我们大致可以了解。在全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。
而诸多的XxxxAutoConfiguration自动配置类,就是Spring容器的JavaConfig形式,作用就是为Spring 容器导入bean,而所有导入的bean所需要的属性都通过xxxxProperties的bean来获得。
可能到目前为止还是有所疑惑,但面试的时候,其实远远不需要回答的这么具体,你只需要这样回答:
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
EnableAutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样。@EnableAutoConfiguration自动配置的魔法骑士就变成了:从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。
SpringBoot的启动原理基本算是讲完了,为了方便记忆,我根据上面的分析画了张图
SpringBoot自动化配置关键组件关系图
mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。
可以发现其最终实现了ImportSelector(选择器)和BeanClassLoaderAware(bean类加载器中间件),重点关注一下AutoConfigurationImportSelector的selectImports方法。
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());}}
该方法在springboot启动流程——bean实例化前被执行,返回要实例化的类信息列表。我们知道,如果获取到类信息,spring自然可以通过类加载器将类加载到jvm中,现在我们已经通过spring-boot的starter依赖方式依赖了我们需要的组件,那么这些组建的类信息在select方法中也是可以被获取到的,不要急我们继续向下分析。
protected List<String>getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes){
List<String> configurations= SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(),this.getBeanClassLoader());
Assert.notEmpty(configurations,"No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");return configurations;}
该方法中的getCandidateConfigurations方法,通过方法注释了解到,其返回一个自动配置类的类名列表,方法调用了loadFactoryNames方法,查看该方法
publicstatic List<String>loadFactoryNames(Class<?> factoryClass,@Nullable ClassLoader classLoader){
String factoryClassName= factoryClass.getName();return(List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}privatestatic Map<String, List<String>>loadSpringFactories(@Nullable ClassLoader classLoader){
MultiValueMap<String, String> result=(MultiValueMap)cache.get(classLoader);if(result!= null){return result;}else{try{
Enumeration<URL> urls= classLoader!= null? classLoader.getResources("META-INF/spring.factories"): ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result=newLinkedMultiValueMap();while(urls.hasMoreElements()){
URL url=(URL)urls.nextElement();
UrlResource resource=newUrlResource(url);
Properties properties= PropertiesLoaderUtils.loadProperties(resource);
Iterator var6= properties.entrySet().iterator();
在上面的代码可以看到自动配置器会根据传入的factoryClass.getName()到项目系统路径下所有的spring.factories文件中找到相应的key,从而加载里面的类。我们就选取这个mybatis-spring-boot-autoconfigure下的spring.factories文件
进入org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration中,主要看一下类头:
发现Spring的@Configuration,俨然是一个通过注解标注的springBean,继续向下看,
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class})这个注解的意思是:当存在SqlSessionFactory.class, SqlSessionFactoryBean.class这两个类时才解析MybatisAutoConfiguration配置类,否则不解析这一个配置类,make sence,我们需要mybatis为我们返回会话对象,就必须有会话工厂相关类。
@CondtionalOnBean(DataSource.class):只有处理已经被声明为bean的dataSource。
@ConditionalOnMissingBean(MapperFactoryBean.class)这个注解的意思是如果容器中不存在name指定的bean则创建bean注入,否则不执行(该类源码较长,篇幅限制不全粘贴)
以上配置可以保证sqlSessionFactory、sqlSessionTemplate、dataSource等mybatis所需的组件均可被自动配置,@Configuration注解已经提供了Spring的上下文环境,所以以上组件的配置方式与Spring启动时通过mybatis.xml文件进行配置起到一个效果。通过分析我们可以发现,只要一个基于SpringBoot项目的类路径下存在SqlSessionFactory.class, SqlSessionFactoryBean.class,并且容器中已经注册了dataSourceBean,就可以触发自动化配置,意思说我们只要在maven的项目中加入了mybatis所需要的若干依赖,就可以触发自动配置,但引入mybatis原生依赖的话,每集成一个功能都要去修改其自动化配置类,那就得不到开箱即用的效果了。所以Spring-boot为我们提供了统一的starter可以直接配置好相关的类,触发自动配置所需的依赖(mybatis)如下:
这里是截取的mybatis-spring-boot-starter的源码中pom.xml文件中所有依赖:
因为maven依赖的传递性,我们只要依赖starter就可以依赖到所有需要自动配置的类,实现开箱即用的功能。也体现出Springboot简化了Spring框架带来的大量XML配置以及复杂的依赖管理,让开发人员可以更加关注业务逻辑的开发。