Spring Boot之——自动配置原理

starter依赖管理机制

目的:通过依赖能了解SpringBoot管理了哪些starter

解析:

  1. 通过依赖 spring-boot-dependencies 搜索 starter- 发现非常多的官方starter,并且已经帮助我们管理好了版本。

  2. 项目中使用直接引入对应的 starter 即可,这个场景下需要的依赖就会自动导入到项目中,简化了繁琐的依赖。

    如果需要修改版本可以有两种方式:

    • 查看spring-boot-dependencies里面规定当前依赖的版本 用的 key
    • 使用Maven依赖管理的就近原则
  3. 引入 starter 不仅仅是帮助我们管理了依赖,还帮我做了很多的默认的配置信息,简化了大量的配置,使用更加的简单。

  4. 所有的场景启动器的底层都依赖 spring-boot-starter

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.3.10.RELEASE</version>
    <scope>compile</scope>
    </dependency>

小结:

  • 引入官方starter依赖默认都可以不写版本
  • 如果配置满足您当前开发需要,则默认配置即可

自动化配置初体验

目的:以web MVC自动化配置原理为例讲解,能够理解web MVC自动化配置加入了哪些依赖,做了哪些默认配置。

解析:

回忆一下:SpringMVC学习时候,我们在 SSM整合时;

添加spring及spring web mvc相关依赖

springmvc.xml 配置文件配置了:

  1. 扫描controller 所在包
  2. 配置annotation-driven支持mvc功能
  3. 视图解析器
  4. 静态资源
  5. 拦截器
  6. ……

web.xml 配置:

  1. 初始化spring容器
  2. 初始化springmvc DispatcherServlet
  3. post请求乱码过滤器

部署还需要单独的tomcat

也就是说:我们现在需要在开发业务代码前,就必须要准备好这些环境,否则无法完成业务代码,这就是我们现在的问题。

让这些问题成为过去,现在我们就探索一下SpringBoot是如何帮助我们完成强大而又简单自动化配置的。

引入 web 开发场景启动器依赖:

1
2
3
4
5
<!--web开发的起步依赖   场景启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

帮助我们做了以下自动化配置:

  1. 依赖版本和依赖什么jar都不需要开发者关注

  2. 自动化配置

    • 自动配好SpringMVC
      • 引入SpringMVC全套组件
      • 自动配好SpringMVC常用组件(三大组件,文件上传等)
    • 自动配好Web常见功能,如:字符编码问题,静态资源管理
    • 默认的包结构扫描
      • 引导类所在包及其下面的所有子包里面的组件都会被默认扫描
      • 无需以前的包扫描配置
      • 想要改变扫描路径@SpringBootApplication(scanBasePackages=”com.itheima”)
      • 或者@ComponentScan 指定扫描路径
  3. 自动配好Tomcat

小结:

  • 有了SpringBoot以后,让开发人员重点关注业务本身,而不是环境上,提升了开发效率。

底层原理-@Configuration配置注解

目的:掌握@Configuration注解的作用及新特性

讲解:

1、@Configuration注解的作用是替代原始 spring配置文件 功能

演示:

1)编写配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itheima.sh.config;

import com.itheima.sh.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 1、@Configuration 替代 spring配置文件(配置bean)
* 2、组件源码中包含 @Component 注解,当前类也会注册到 IOC 容器,默认类名小写
* 3、默认都是单例的
*/
@Configuration
public class MyConfig {

@Bean // 默认方法名称作为容器中的name
public User getUser() {
return new User();
}
}

2)在引导类编写代码测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication
@MapperScan(basePackages = "com.itheima.sh.mapper")
public class DataApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DataApplication.class, args);

// 根据name获取容器中的bean
User user1 = applicationContext.getBean("getUser", User.class);
User user2 = applicationContext.getBean("getUser", User.class);
System.out.println(user1 == user2);

MyConfig myConfig1 = applicationContext.getBean("myConfig", MyConfig.class);
MyConfig myConfig2 = applicationContext.getBean("myConfig", MyConfig.class);
System.out.println(myConfig1 == myConfig2);
// 注意:如果 MYConfig配置类没有按照规范编写,则容器中bean 的name为 类名
}
}

SpringBoot 提供一个注解和当前注解功能一样:@SpringBootConfiguration

2、proxyBeanMethods:代理bean的方法属性(since spring 5.2以后)

image-20210503222348291

功能:

  • proxyBeanMethods = true:Full模式,保证每个@Bean方法被调用多少次返回的组件都是单实例的

  • proxyBeanMethods = false:Lite模式,每个@Bean方法被调用多少次返回的组件都是新创建的

演示:

  1. 默认 proxyBeanMethods=true,springBoot会检查这个组件是否在容器中有,有则直接引用
1
2
3
// 默认 proxyBeanMethods=true springBoot会检查这个组件是否在容器中有,有则直接引用
User user3 = myConfig1.getUser();
System.out.println(user1 == user3); // true
  1. 修改 proxyBeanMethods=false,则每调用一次Spring就会创建一个新的Bean对象

image-20210503222942021

在执行结果则为 false, 证明两次获取的bean不是同一个bean。

小结:

  • 组件依赖必须使用Full模式默认。其他默认是否Lite模式

  • Full模式每次都会检查bean,效率较Lite模式慢

底层原理-@Import注解使用1

目的:能够理解@Import注解作用及4种使用方式

讲解:

作用:使用@Import导入的类会被Spring加载到IOC容器中

@Import提供4中用法:

  1. 导入Bean
  2. 导入配置类
  3. 导入 ImportSelector 实现类。一般用于加载配置文件中的类
  4. 导入 ImportBeanDefinitionRegistrar 实现类

实现:

1、导入Bean

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
package com.itheima.sh;

import com.itheima.sh.pojo.User;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

import java.util.Map;

@SpringBootApplication
@Import(User.class)
//会自动执行当前类的构造方法创建对象,存到IOC容器, bean名称为:类的全路径

public class DataApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DataApplication.class, args);

Map<String, User> map = applicationContext.getBeansOfType(User.class);
System.out.println(map);

User user1 = applicationContext.getBean("com.itheima.sh.pojo.User", User.class);
System.out.println(user1);

}
}

2、导入配置类

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
package com.itheima.sh;

import com.itheima.sh.config.MyConfig;
import com.itheima.sh.pojo.User;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

import java.util.Map;

@SpringBootApplication
@MapperScan(basePackages = "com.itheima.sh.mapper")
//@Import(User.class)
//1、会自动执行当前类的构造方法创建对象,存到IOC容器, bean名称为:类的全路径

@Import(MyConfig.class)
//2、创建MyConfig bean,并且类中有 带有@Bean注解方法,创建对象存到IOC容器,bean名称为:默认方法名称
public class DataApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DataApplication.class, args);

//{getUser=com.itheima.sh.pojo.User@1b4a3a1}
Map<String, User> map = applicationContext.getBeansOfType(User.class);
System.out.println(map);

User user1 = applicationContext.getBean("getUser", User.class);
System.out.println(user1);

Map<String, MyConfig> config = applicationContext.getBeansOfType(MyConfig.class);
//{com.itheima.sh.config.MyConfig=com.itheima.sh.config.MyConfig@7e848aea}
System.out.println(config);
}
}

底层原理-@Import注解使用2

目的:讲解@Import注解使用另外两种使用方式

步骤:

  1. 导入 ImportSelector 实现类。一般用于加载配置文件中的类
  2. 导入 ImportBeanDefinitionRegistrar 实现类

实现:

导入 ImportSelector 实现类。一般用于加载配置文件中的类

1、编写 ImportSelector 实现类,MyImportSelector

2、引导类导入

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
package com.itheima.sh;

import com.itheima.sh.config.MyImportSelector;
import com.itheima.sh.pojo.User;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

import java.util.Map;

@SpringBootApplication
@MapperScan(basePackages = "com.itheima.sh.mapper")
//@Import(User.class)
//1、会自动执行当前类的构造方法创建对象,存到IOC容器, bean名称为:类的全路径

//@Import(MyConfig.class)
//2、创建MyConfig bean,并且类中有 带有@Bean注解方法,创建对象存到IOC容器,bean名称为:默认方法名称

@Import(MyImportSelector.class)
//3、创建MyConfig bean 名称为:类名全路径,创建带有@Bean注解方法实例,名称为:方法名称
public class DataApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DataApplication.class, args);

Map<String, MyConfig> map = applicationContext.getBeansOfType(MyConfig.class);
//{com.itheima.sh.config.MyConfig=com.itheima.sh.config.MyConfig@44384b4a}
System.out.println(map);

Map<String, User> userMap = applicationContext.getBeansOfType(User.class);
//{getUser=com.itheima.sh.pojo.User@5cc3e49b}
System.out.println(userMap);

}
}

导入 ImportBeanDefinitionRegistrar 实现类

1、编写 ImportBeanDefinitionRegistrar 实现类,MyImportBeanDefinitionRegistrar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itheima.sh.config;

import com.itheima.sh.pojo.User;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

/**
* @param importingClassMetadata 导入类的元注解信息
* @param registry Bean注册表
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
.rootBeanDefinition(User.class).getBeanDefinition();
registry.registerBeanDefinition("user", beanDefinition);
}
}

2、引导类测试

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
package com.itheima.sh;

import com.itheima.sh.config.MyImportBeanDefinitionRegistrar;
import com.itheima.sh.pojo.User;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

import java.util.Map;

@SpringBootApplication
@MapperScan(basePackages = "com.itheima.sh.mapper")

//@Import(User.class)
//1、会自动执行当前类的构造方法创建对象,存到IOC容器, bean名称为:类的全路径

//@Import(MyConfig.class)
//2、创建MyConfig bean,并且类中有 带有@Bean注解方法,创建对象存到IOC容器,bean名称为:默认方法名称

//@Import(MyImportSelector.class)
//3、创建MyConfig bean 名称为:类名全路径,创建带有@Bean注解方法实例,名称为:方法名称

@Import(MyImportBeanDefinitionRegistrar.class)
//4、创建Bean,名称:在registerBeanDefinition中定义
public class DataApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(DataApplication.class, args);

Map<String, User> userMap = applicationContext.getBeansOfType(User.class);
//{user=com.itheima.sh.pojo.User@23c7cb18}
System.out.println(userMap);

}
}

小结:

  • 讲解当前小节的目的主要是为源码准备
  • 还有我们也可以知道创建Bean对象,还可以使用@Import四种方式

底层原理-@Conditional衍生条件装配

目的:理解@Conditional衍生条件装配的作用

讲解:

作用:条件装配,满足Conditional指定的条件,则进行组件注入,初始化Bean对象到IOC容器

image-20210503232237027

演示:

在RedisConfig类中添加注释:

方法中定义:

image-20210503233346921

类上定义:

image-20210506110554269

注意:也可以添加到 类上, 满足条件则类及类中的对象生效。

小结:

  • @ConditionalOnXXX 注解存在的意义是:满足条件当前类或者Bean才有效,按需导入。

底层原理-@ConfigurationProperties配置绑定

目的:

回顾 @ConfigurationProperties配置绑定 存在的目的是:获取配置属性或者是配置文件指定前缀的属性信息,并且初始化Bean对象到 IOC 容器。

由此我们可以想:将来的配置我们可以放在配置文件中,通过这个注解来读取并封装成对象

image-20210503234111133

自动化配置原理-@SpringBootApplication入口分析

目的:能够理解SpringBoot自动化配置流程中@SpringBootApplication是一个组合注解,及每一个注解的作用能够知道作用。

讲解:

1、SpringBoot是一个组合注解

image-20210503234649542

2、@SpringBootConfiguration注解作用

  • @SpringBootConfiguration是对@Configuration注解的包装,proxyBeanMethods 默认配置 true, full模式(单例Bean)

  • 标识是一个配置类,所以 引导类也是配置类

3、@ComponentScan注解作用

  • 组件扫描,默认扫描的规则 引导类所在的包及其子包所有带注解的类

问题:

  1. 在引导类中配置 @Bean 注解可以吗?
  2. 为什么Controller、service类添加完注解后,不需要添加扫描包?

自动化配置原理-@EnableAutoConfiguration自动配置注解

目的:理解@EnableAutoConfiguration自动化配置核心实现注解

讲解:

1、@EnableAutoConfiguration是一个组合注解

image-20210503235551587

2、@AutoConfigurationPackage注解作用

作用:利用Registrar给容器中导入一系列组件

image-20210503235747889

点击 Registrar 进入到源码的 register 方法,添加 断点,测试

image-20210504001213912

通过 debug 程序发现,默认情况下 将引导类的所有包及其子包的组件导入进来

3、@Import(AutoConfigurationImportSelector.class)注解作用

作用:是利用selectImports方法中的 getAutoConfigurationEntry 方法给容器中批量导入相关组件

调用流程分析:

  1. 调用AutoConfigurationImportSelector类中的selectImports方法

  2. 调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类

  3. 利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader)得到所有的组件

  4. 从META-INF/spring.factories位置来加载一个文件。

    默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件

    image-20210504100751196

    spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

    image-20210504100911351

通过这个配置文件加载的自动配置:当前版本(2.3.10)是有127个默认的自动化配置

image-20210504100635289

小结:

  • 自动化配置默认加载的配置文件在哪?

自动化配置原理-按条件开启自动配置类和配置项

目的:

  • 能够理解所有的自动化配置虽然会全部加载,底层有大量的@ConditionalOnXXX,有很多自动配置类并不能完全开启。
  • 如果配置生效了,则会加载默认的属性配置类,实现默认的对应场景的自动化配置

讲解:

1、以上通过 META-INF/spring.factories 配置文件找到所有的自动化配置类,但 是不是全部的生效的呢?很显然是不可能全部都生效的。

2、以 RedisAutoConfiguration 为例讲解, 进入到 RedisAutoConfiguration 自动化配置类。

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
// 配置类,Lite模式
@Configuration(proxyBeanMethods = false)
//**条件装配注解,在有RedisOperations类的情况下当前类被初始化,并创建方法Bean
@ConditionalOnClass(RedisOperations.class)
//如果条件满足:开始加载自动化配置的属性值 RedisProperties
//猜想:Redis做自动化给我配置了哪些属性呢?
//答案:
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

}

3、RedisProperties 属性对象,用于加载默认的配置,如果配置文件配置了该属性,则配置文件就生效。

image-20210504105806400

4、LettuceConnectionConfiguration 是当前版本默认使用的Redis的连接池(性能较好)

image-20210504110100549

当然可以切换为 Jedis:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

5、类中 实例化 了两个Bean对象

1
2
3
4
5
6
7
8
9
10
@Bean  // 默认容器Bean名称:redisTemplate
@ConditionalOnMissingBean(name = "redisTemplate")
//容器中没有名称:redisTemplate Bean的时候,当前方法执行创建Bean并存到IOC容器
public RedisTemplate<Object, Object> redisTemplate(){}


@Bean // 默认容器Bean名称:stringRedisTemplate
@ConditionalOnMissingBean
// 容器中没有Bean时,则创建Bean
public StringRedisTemplate stringRedisTemplate(){}

验证:我们可以在我们自己的项目里面创建一个 redisTemplate Bean,看容器创建的Bean执行的是哪一个方法。

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
@Configuration
public class RedisConfig {

@Bean
public RedisSerializer<Object> redisKeySerializer() {
return new Jackson2JsonRedisSerializer<Object>(Object.class);
}

@Bean
public RedisSerializer<Object> redisValueSerializer() {
return new Jackson2JsonRedisSerializer<Object>(Object.class);
}

/**
* RedisTemplate配置
* @param factory
*/
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory
, RedisSerializer<Object> redisKeySerializer, RedisSerializer<Object> redisValueSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);

redisTemplate.setDefaultSerializer(redisValueSerializer);
redisTemplate.setKeySerializer(redisKeySerializer);
redisTemplate.setHashKeySerializer(redisKeySerializer);
redisTemplate.afterPropertiesSet();
System.out.println("RedisConfig redisTemplate is init......");
return redisTemplate;
}
}

结果:保证容器中只有一个 Bean 实例

image-20210504111552833

问题:

  • 这些不用的 starter 的依赖,能不能导入到我们工程里面? 为什么?

自动化配置原理-debug全流程

目的:能够理解整个SpringBoot启动的完成自动化配置及属性加载的全过程

自动化配置原理-总结

SpringBoot自动化配置流程总结:

  • 程序启动找到自动化配置包下 META-INF/spring.factoriesEnableAutoConfiguration
  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
  • 生效的配置类就会给容器中装配很多组件
  • 只要容器中有这些组件,相当于这些功能就有了
  • 定制化配置
    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。
1
2
3
4
graph LR;
1[xxxxAutoConfiguration] --> 2[ Bean组件]
2 --> 3[xxxxProperties里面取值]
3 --> 4[application.properties]

开发人员使用步骤总结:

  • 引入场景依赖

  • 查看自动配置了哪些(选做)

    • 自己分析,引入场景对应的自动配置一般都生效了
    • 配置文件中debug=true开启自动配置报告。Negative(不生效)\Positive(生效)
  • 自己分析是否需要修改

    • 参照文档修改配置项,xxxxProperties绑定了配置文件的哪些。
    • 自定义加入或者替换组件,@Bean、@Component等