springboot自动配置原理
springboot的出现就是为了简化Spring框架的开发,快速构建java项目。
springboot的两个核心特性就是起步依赖和自动配置。
起步依赖原理:maven的依赖传递
自动配置原理:条件注入、SPI机制
起步依赖
假如我们没有使用SpringBoot,用的是Spring框架进行web程序的开发,此时我们就需要引入web程序开发所需要的一些依赖。
spring-webmvc依赖:这是Spring框架进行web程序开发所需要的依赖
servlet-api依赖:Servlet基础依赖
jackson-databind依赖:JSON处理工具包
如果要使用AOP,还需要引入aop依赖、aspect依赖
项目中所引入的这些依赖,还需要保证版本匹配,否则就可能会出现版本冲突问题。
如果我们使用了SpringBoot,就不需要像上面这么繁琐的引入依赖了。我们只需要引入一个依赖就可以了,那就是web开发的起步依赖:spring-boot-starter-web。
为什么我们只需要引入一个web开发的起步依赖,web开发所需要的所有的依赖都有了呢?
- 因为Maven的依赖传递。
在SpringBoot给我们提供的这些起步依赖当中,已提供了当前程序开发所需要的所有的常见依赖(官网地址:https://docs.spring.io/spring-boot/docs/2.7.7/reference/htmlsingle/#using.build-systems.starters)。
比如:spring-boot-starter-web,这是web开发的起步依赖,在web开发的起步依赖当中,就集成了web开发中常见的依赖:json、web、webmvc、tomcat等。我们只需要引入这一个起步依赖,其他的依赖都会自动的通过Maven的依赖传递进来。
结论:起步依赖的原理就是Maven的依赖传递。
自动配置
自动配置即根据开发者添加的jar包依赖,会自动将一些配置类的bean注册到IOC容器内。使用时只需方便地添加@Autowire或者@Resource等注解即可使用。
要了解SpringBoot的自动配置原理,我们必须先来了解SPI机制和条件注入是什么。
SPI机制
1、什么是SPI
SPI又分为JDK SPI和Spring SPI
SPI全称为Service Provider Interface,是JDK内置的一种服务提供发现机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。
在 Java 平台上,SPI 通常是通过 java.util.ServiceLoader 类实现的,但 Spring Boot 对这一概念进行了扩展,以支持其自动配置和模块化架构。JDK中一个经典的SPI就是jdbc接口,
JDK SPI
SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展。
举个例子:
创建一个PhoneService接口:
public interface PhoneService {
void brand();
}
分别创建两个实现类:
public class HuaweiPhoneService implements PhoneService {
@Override
public void brand() {
System.out.println("华为手机");
}
}
public class XiaomiPhoneService implements PhoneService {
@Override
public void brand() {
System.out.println("小米手机");
}
}
在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.my.demo.service.PhoneService
,里面加上我们需要用到的实现类
在 Java 平台上,SPI 通常是通过 java.util.ServiceLoader
类实现的,我们来写个测试类测试一下:
public class DemoApplication {
public static void main(String[] args) {
ServiceLoader<PhoneService> loaders = ServiceLoader.load(PhoneService.class, null);
for (PhoneService phoneService : loaders) {
phoneService.brand();
}
}
}
测试:
原理:
Java中的 SPI 机制就是在每次类加载的时候会先去找到classpath下的 META-INF
文件夹下的 services 文件夹下的文件
将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类
找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,然后可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。
应用场景
JDBC DriverManager
在JDBC4.0之前,连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。
JDBC接口定义
首先在java中定义了接口java.sql.Driver
,并没有具体的实现,具体的实现都是由不同厂商来提供的。
mysql实现
在mysql的jar包中,可以找到META-INF/services
目录,该目录下会有一个名字为java.sql.Driver
的文件,文件内容是com.mysql.cj.jdbc.Driver
,这里面的内容就是针对Java中定义的接口的实现。
现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用Class.forName("com.mysql.jdbc.cj.Driver")
来加载驱动了,而是直接使用如下代码:
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url, username, password);
...
Java使用SPI扩展机制来查找驱动相关的东西,关于驱动的查找其实都在DriverManager
中,DriverManager
是Java中的实现,用来获取数据库连接,在DriverManager
中的相关代码如下:
Java通过SPI机制,会搜索classpath下以及jar包中所有的META-INF/services
目录下的java.sql.Driver
文件,并找到文件中的实现类的名字,根据驱动名字实例化各个实现类,完成驱动的注册
Spring Boot SPI
介绍
Spring Boot 对SPI机制这一概念进行了扩展,以支持其自动配置和模块化架构。
Spring Boot 利用 spring.factories (注意:从 SpringBoot 2.7 起自动配置不推荐使用 /META-INF/spring.factories 文件,而是在/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)文件,这个文件列出了与自动配置相关的接口及其实现类,Spring Boot 启动时会加载这些配置。
spring.factories 文件
这个文件使用键值对的格式列出了多种服务类型及其对应的实现类,常见的服务类型包括:
-
org.springframework.boot.autoconfigure.EnableAutoConfiguration:用于自动配置。
-
org.springframework.context.ApplicationListener:用于应用事件监听器。
-
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider:用于模板引擎的可用性判断。
以下是 spring.factories 文件的一个典型例子:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.JpaAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.context.ApplicationListener=\
com.example.MyApplicationListener
Spring Boot 3 则要在 resources/META_INFO/spring/ 目录下新建
org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,并填入需要自动配置的
类的全路径:
com.example.JpaAutoConfiguration
不使用SPI机制
我们先来看一下不使用SPI机制怎么来实现bean的配置:
新建一个模块myhello,写一个MyHelloDemo类,放入IOC容器:
@Component
public class MyHelloDemo {
public void myHello() {
System.out.println(" hello, you are using MyHelloDemo ");
}
}
创建一个测试模块,引入myhello模块的依赖,创建引导类,和测试类:
@SpringBootApplication
public class TestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TestDemoApplication.class, args);
}
}
@SpringBootTest
public class MyTest {
// IOC容器对象
@Autowired
private ApplicationContext applicationContext;
@Test
public void test() {
try {
MyHelloDemo bean = applicationContext.getBean(MyHelloDemo.class);
bean.myHello();
} catch (Exception e) {
System.err.println(" MyHelloDemo.class not found in IOC");
}
}
}
这里我们通过ApplicationContext来拿到IOC容器里面的bean
此时运行测试类会发现找不到bean对象:
引入进来的第三方依赖当中的bean以及配置类为什么没有生效?
- 原因是因为,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。
- SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。
- 当前包:com.test.demo, 第三方依赖中提供的包:com.myhello.demo(扫描不到)
那么如何解决以上问题的呢?
- 方案1:@ComponentScan 组件扫描
- 方案2:@Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)
方案一
@ComponentScan 组件扫描
@SpringBootApplication
@ComponentScan(basePackages = {"com.test","com.myhello"})
public class TestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TestDemoApplication.class, args);
}
}
重新测试,会发现可以输出了:
缺点:当需要引入大量的第三方依赖,就需要在启动类上配置大量要扫描的包,这种方式会很繁琐
方案二
@Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)
导入形式主要有以下几种:
- 导入普通类
- 导入配置类
- 导入ImportSelector接口实现类
1、导入普通类:
导入的类会被Spring加载到IOC容器中
@Import(MyHelloDemo.class)
@SpringBootApplication
public class TestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TestDemoApplication.class, args);
}
}
测试:
2、导入配置类
-
配置类
去掉MyHelloDemo类上的@Component注解,新建一个配置类,自定义bean
@Configuration
public class MyHelloConfig {
@Bean
public MyHelloDemo myHelloDemo() {
return new MyHelloDemo();
}
}
- 启动类
@SpringBootApplication
@Import(MyHelloConfig.class)
public class TestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TestDemoApplication.class, args);
}
}
测试:
3、导入ImportSelector接口实现类
- ImportSelector接口实现类
public class MyHelloImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.myhello.demo.MyHelloDemo"};
}
}
- 启动类
@Import(MyHelloImportSelector.class)
@SpringBootApplication
public class TestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TestDemoApplication.class, args);
}
}
测试:
我们使用@Import注解通过这三种方式都可以导入第三方依赖中所提供的bean或者是配置类。
问题:如果基于以上方式完成自动配置,当要引入一个第三方依赖时,是不是还要知道第三方依赖中有哪些配置类和哪些Bean对象?相当麻烦!
当我们要使用第三方依赖,依赖中到底有哪些bean和配置类,谁最清楚?
- 答案:第三方依赖自身最清楚。
结论:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。
怎么让第三方依赖自己指定bean对象和配置类?
- 比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都以@EnableXxx开头的注解,注解中封装的就是@Import注解
4、使用第三方依赖提供的 @EnableXxx注解
- 第三方依赖中提供的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
//指定要导入哪些bean对象或配置类
@Import(MyHelloImportSelector.class)
public @interface EnableMyHelloConfig {
}
- 在使用时只需在启动类上加上@EnableXxxxx注解即可
// 使用第三方依赖提供的Enable开头的注解
@EnableMyHelloConfig
@SpringBootApplication
public class TestDemoApplication {
public static void main(String[] args) {
SpringApplication.run(TestDemoApplication.class, args);
}
}
测试:
以上四种方式都可以完成导入操作,但是第4种方式会更方便更优雅,而这种方式也是SpringBoot当中所采用的方式。
使用SPI机制
ok,这种手动配置bean是不是相当麻烦,我用你的第三方框架,最好不要让我配置,我就像引入依赖后,直接DI注入使用,这才方便呀。
那下面来看看使用SpringBoot SPI机制实现自动配置:
再新建一个模块mydemo,创建一个MyDemoService接口,再定义一个实现类:
public interface MyDemoService {
void sayHello();
}
public class MyDemoServiceImpl implements MyDemoService {
@Override
public void sayHello() {
System.out.println(" hello, MyDemoService... ");
}
}
定义一个配置类;
@Configuration
public class MyDemoConfig {
@Bean
public MyDemoService myDemoService() {
return new MyDemoServiceImpl();
}
}
关键点:再resources目录下创建META-INF/spring
文件夹,新建org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,里面写上MyDemoConfig的全路径,这是实现自动配置的一个关键步骤
在testdemo测试模块引入mydemo依赖,新建一个测试方法:
@Test
public void test2() {
try {
MyDemoService bean = applicationContext.getBean(MyDemoService.class);
System.out.println(" found MyDemoService.class in IOC ");
bean.sayHello();
} catch (Exception e) {
System.err.println(" MyDemoService.class not found in IOC... ");
}
}
测试:可以成功执行
此时会发现,我们做的操作只是引入了相关依赖,无需再手动配置,就可以通过IOC容器或DI依赖拿到bean对象,操作方法,这就是SpringBoot自动配置的强大之处。
条件注入
Spring Boot的自动配置还支持条件注入,在自动配置类声明bean的时候,除了在方法上加了一个@Bean注解以外,还会经常用到一个注解,就是以Conditional开头的这一类的注解。以Conditional开头的这些注解都是条件装配的注解。
@ConditionalXxx注解:
- 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。
- 位置:方法、类
- @ConditionalXxx本身是一个父注解,派生出大量的子注解:
- @ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。
- @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。
- @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。
下面来尝试一下:
@ConditionalOnClass(name = "com.test.demo.Hello")
环境中有对应字节码文件,才注册bean到IOC容器
@Configuration
@ConditionalOnClass(name = "com.test.demo.Hello")
public class MyDemoConfig {
@Bean
@ConditionalOnMissingBean
public MyDemoService myDemoService() {
return new MyDemoServiceImpl();
}
}
测试:此时会发现IOC容器中没有MyDemoService的实例对象了
在com.test.demo包下创建Hello类,再次运行就可以找到了:
再来尝试一个@ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。
修改MyDemoConfig类上的注解,此时配置文件中存在mydemo.config.enable属性,并且对应的值为true时才装配这个bean到IOC容器:
@Configuration
//@ConditionalOnClass(name = "com.test.demo.Hello")
@ConditionalOnProperty(prefix = "mydemo.config", name = "enable",havingValue = "true")
public class MyDemoConfig {
@Bean
@ConditionalOnMissingBean
public MyDemoService myDemoService() {
return new MyDemoServiceImpl();
}
}
运行测试类:会发现找不到bean了
在配置文件中配置相关属性:
此时再去运行测试类,就可以找到bean了:
这就是
自动配置源码分析
要搞清楚SpringBoot的自动配置原理,要从SpringBoot启动类上使用的核心注解
@SpringBootApplication开始分析:
在@SpringBootApplication注解中包含了:
- 元注解(不再解释)
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
我们先来看第一个注解:@SpringBootConfiguration
@SpringBootConfiguration注解上使用了@Configuration,表明SpringBoot启动类就是一个配置类。
@Indexed注解,是用来加速应用启动的(不用关心)。
接下来再先看@ComponentScan注解:
@ComponentScan注解是用来进行组件扫描的,扫描启动类所在的包及其子包下所有被@Component及其衍生注解声明的类。
SpringBoot启动类,之所以具备扫描包功能,就是因为包含了@ComponentScan注解。
最后我们来看看@EnableAutoConfiguration注解(自动配置核心注解):
使用@Import注解,导入了实现ImportSelector接口的实现类。
AutoConfigurationImportSelector类是ImportSelector接口的实现类。
AutoConfigurationImportSelector类中重写了ImportSelector接口的selectImports()方法:
selectImports()方法底层调用getAutoConfigurationEntry()方法,获取可自动配置的配置类信息集合
getAutoConfigurationEntry()方法通过调用getCandidateConfigurations(annotationMetadata, attributes)方法获取在配置文件中配置的所有自动配置类的集合
getCandidateConfigurations方法的功能:
获取所有基于META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件、META-INF/spring.factories文件中配置类的集合
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件和META-INF/spring.factories文件这两个文件在哪里呢?
- 通常在引入的起步依赖中,都有包含以上两个文件
自动配置原理源码入口就是@SpringBootApplication注解,在这个注解中封装了3个注解,分别是:
- @SpringBootConfiguration
- 声明当前类是一个配置类
- @ComponentScan
- 进行组件扫描(SpringBoot中默认扫描的是启动类所在的当前包及其子包)
- @EnableAutoConfiguration
- 封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类)
- 在实现类重写的selectImports()方法,读取当前项目下所有依赖jar包中META-INF/spring.factories、META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports两个文件里面定义的配置类(配置类中定义了@Bean注解标识的方法)。
- 封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类)
当SpringBoot程序启动时,就会加载配置文件当中所定义的配置类,并将这些配置类信息(类的全限定名)封装到String类型的数组中,最终通过@Import注解将这些配置类全部加载到Spring的IOC容器中,交给IOC容器管理。
但是不是所有的bean都会被注册到IOC容器中
最后呢给大家抛出一个问题:在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中定义的配置类非常多,而且每个配置类中又可以定义很多的bean,那这些bean都会注册到Spring的IOC容器中吗?
答案:并不是。 在声明bean对象时,上面有加一个以@Conditional开头的注解,这种注解的作用就是按照条件进行装配,只有满足条件之后,才会将bean注册到Spring的IOC容器中
总结
spring boot的两个核心特性:起步依赖和自动配置
起步依赖的原理:maven的依赖传递
自动配置的原理:条件注入、SPI机制
spring boot应用启动时,会加载主配置类,也就是被@SpringBootApplication注解修饰的引导类,该注解组合了三个注解@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。
@EnableAutoConfiguration这个注解才是自动配置的核心。
- 它组合了一个@Import注解,Import注解里面指定了一个ImportSelector接口的实现类。
- 在这个实现类中,重写了ImportSelector接口中的selectImports()方法。
- 而selectImports()方法中会去读取所有jar包中的一个配置文件,并将该配置文件中定义的配置类做为selectImports()方法的返回值返回,返回值代表的就是需要将哪些类交给Spring的IOC容器进行管理。
SpringBoot 2.7 之前自动配置使用 /META-INF/spring.factories 文件
SpringBoot 2.7之后的版本使用/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
- 所有自动配置类的中声明的bean并不是都会加载到Spring的IOC容器中,这些配置类中在声明bean时,通常都会添加@Conditional开头的注解,这个注解就是进行条件装配。而Spring会根据Conditional注解有选择性的进行bean的创建。
核心:
bean是怎么装配的
-
SPI机制:把要自动装配的类放在了什么地方:要自动装配的类放到了文件里
-
springboot 3的存放位置
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
-
springboot 2的存放位置
META-INF/spring.factories
-
-
文件里放了要自动装配的类的全限定名,通过IO流把这些类的全限定名读出来,通过反射获取类的构造器创建Bean对象,再往IOC容器里面放对应的bean
-
条件装配,不是所有的bean都装配