SpringBoot 2.0
一、了解自动配置原理
1、SpringBoot的特点
1.1、依赖管理
在传统Maven项目开发中,我们要实现什么功能就要导入相应的jar包。在我们的开发场景中可能要导入很多的jar包,引入的外部jar包间可能相互依赖,我们还要去查看不同的jar包版本是否存在冲突。SpringBoot使用父项目做依赖管理,在使用Spring Initializr创建一个新项目时,在pom文件中都会引入一个父依赖。在父依赖中定义了开发过程中几乎所有场景所需要jar包的版本(这些版本是相互兼容的)。这样在我们开发过程中,就不会因jar包间版本冲突给我们造成困扰。下面我们来看看SpringBoot是怎么做的吧。
使用父依赖进行依赖管理
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
spring-boot-starter-parent也引入了父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
</parent>
这里面几乎声明了开发中常用jar包的版本号,自动版本仲裁机制
spring-boot-dependencies中的<dependencyManagement>标签几乎包含了我们开发所需要的所有依赖。<properties>标签定义了依赖的版本
dependencyManagement粗了解
在Maven中的作用其实相当于一个对所依赖jar包进行版本管理的管理器。在==dependencyManagement下申明的dependencies,Maven并不会去实际下载所依赖的jar,而是在dependencyManagement中用一个Map记录了jar的三维坐标。==而被dependencies包裹的元素,Maven是会去仓库实际下载所需要的jar包的,而至于需要下载什么版本的jar包就有两种判断途径:
1、如果dependencies里的dependency自己没有声明version元素,那么maven就会到里面去找有没有对该artifactId和groupId进行过版本声明,如果有,就继承它,如果没有就会报错,告诉你必须为dependency声明一个version
2、如果dependencies中的dependency声明了version,那么无论dependencyManagement中有无对该jar的version声明,都以dependency里的version为准。
所以在我们使用SpringBoot开发的时候,我们在引入外部依赖。没有定义版本,他会去里面去找有没有对该artifactId和groupId进行过版本声明,有的话就去继承它(版本)。
勿需关注版本号
1、引入依赖默认都可以不写版本(可以减少jar包冲突带来的麻烦)
2、引入非版本仲裁的jar,要写版本号。
版本号可以修改 (不推荐)
SpringBoot的将所有依赖的版本定义在父项目的标签中,但是在MAVEN的版本仲裁机制会优先选择子项目下的版本,所以我们只需要在子项目(自己创建的项目)下的pom文件中定义所需要的版本即可。
版本一般定义在下面(spring-boot-dependencies)这个jar包中
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
</parent>
以lombak 为例:
在spring-boot-dependencies的pom文件中,它的版本的定义为:
<dependencyManagement>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependencyManagement>
lombok.version定义在
<properties>
<lombok.version>1.18.24</lombok.version>
</properties>
MAVEN的版本仲裁机制会优先选择子项目下的版本
所以我们只需要在子项目(自己创建的项目)下的pom文件中定义所需要的版本即可
<properties>
<lombok.version>1.18.20</lombok.version>
</properties>
另外SpringBoot也不像Maven一样以功能作为依赖进行导入(想要什么实现功能导入什么依赖),它是以开发的场景作为依赖进行导入,一个开发场景肯定需要实现很多功能,好处就是我们只要引入场景启动器,SpringBoot就会自动帮我们导各个功能所需要的依赖,就不用我们一个一个去导入了。
开发导入starter场景启动器
1、见到很多 spring-boot-starter-* : *就某种场景
2、只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
3、SpringBoot所有支持的场景
https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter
4、见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
5、所有场景启动器最底层的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
总结
Springboot通过父依赖的来做版本仲裁,通过各种场景启动器来加载开发所需要的jar包。
1.2、自动配置
要想实现自动配置,需要完成两大难题,一是导入依赖、二是配置组件。导入依赖我们在前面已经了解过了,我们在导入场景启动器时,SpringBoot就会自动帮我们导入所需要的依赖。
首先我们来看看SpringBoot帮我们做了那些自动配置(先了解帮我们做了什么,再了解怎么做到的)
SpringBoot先帮我们配置好了SpringMVC
1、SpringBoot先帮我们导入了SpringMVC的依赖
2、自动配置好了SpringMVC的常用组件(功能)
自动配置好web的常见功能
Springboot帮我们配置好了所有web开发场景。 比如字符编码问题(characterEncodingFilter),文件上传(multipartResolver)
SpringBoot帮我们配置好了默认的包扫描路径
SpringBoot存在默认的包扫描路径,它将主程序类(被@SpringBootApplication修饰的类)所在的包视为默认的包扫描路径,在程序加载时,会将扫描包及其子包下的组件加入到容器中。下面是Spring官网对默认的包扫描路径的一些解释。
官网上面明确表示,如果程序没有声明包扫描路径,会默认将主程序所在的包当作包扫描路径,但是不推荐使用默认的包扫描路径。我们可以通过@SpringBootApplication注解的scanBasePackages属性来指定包扫描路径。或者@ComponentScan 指定扫描路径,不指定的话默认从注解修饰类所在的包开始扫描。
上面SprinBoot帮我们做的自动配置,它是怎么做到的呢?回顾到我们的SSM时代,我们配置组件一般是将组件的配置信息(比如数据源的一些信息啊)放在配置文件(.properties)上,然后在我们在创建组件时,引入配置文件,取出配置文件的值放到组件中。这样我们就为组件赋上了值。
SpringBoot的配置
各种配置都有默认值
- 默认的配置都会映射到配置类中:比如RedisProperties。
- 配置文件的值最终会绑定到对应配置类的属性中。
- 配置类会创建对象并被加载到容器中去。
按需加载所有自动配置项
- 引入 非常多的starte(场景启动器)
- 引入了哪些场景(starte)这个场景的自动配置才会开启、创建并加载到容器中
- SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure包里面
2、容器功能
2.1、向容器添加组件
1、@Configuration+@Bean
@Configuration注解放在类上,表示这是一个配置类。它是一个复合注解,我们点进去会发现它发现它包含了@Component注解,说明它也会在程序启动时,创建出对象,将它作为组件加载到容器中去。
- 基本使用:(配置类必须放包扫描路径下(它本身也是一个组件,会在程序加载时,放到容器中起),否则无法生效)
/**
*配置类使用@Bean给容器注入的组件,默认是单实例的(无论从容器中取多少次,取的都是同一个对象)的。
*
*/
@Configuration//告诉SpringBoot,这是一个配置类(配置类也会被加到容器中去)=====相当于SSM框架时代的配置文件
public class MyConfig {
@Bean//给容器添加组件,返回类型是组件类型,方法名是组件名,返回值就是组件在容器中的实例
public DogPojo MyDog() {
return new DogPojo("花花", 3, "公");
}
}
- Full模式与Lite模式:@Configuration有一个属性proxyBeanMethods,默认值是true
proxyBeanMethods:代理bean的方法
- Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
- Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
- 组件依赖必须使用Full模式默认。其他默认是否Lite模式
区别:
Full模式下, @Configuration注解下的类加入到容器中就不再是它本生这个类,而是经过代理之后的代理类。此时从代理类中通过配置类对象.方法()的形式获取对象,它会优先从容器获取。
区别:
Full模式下, @Configuration下的类加入到容器中就不再是它本生这个类,而是经过代理之后的代理类。此时从代理类中通过配置类对象.方法()的形式获取对象,它会优先从容器获取。
Lite模式下,@Configuration下的类加入到容器中就是它原本的这个类从代理类中通过配置类对象.方法()的形式获取对象,就是普通的创建对象方式。获取到的对象和容器中的对象(组件)半毛钱关系都没有。
常用场景:在代理类中,向容器添加的组件之间没有相互依赖,那么采用Lite模式会加快程序加载的速度。但是配置类中,向容器添加的组件存在依赖关系,那么就采用Full模式确保组件之间的依赖。
2、包扫描(指定包扫描路径)+标识注解(@Component、@Controller、@Service、@Repository)
这种是我们最常用的将组件加到Spring容器的方式,相信大家很熟悉了。SpringBoot存在默认的包扫描机制,所以我们只需要将需要加到容器中的类放到包扫描的路径下,并在这些类下面添加这些标识注解即可。
3、@Import
@Import的三种用法
进入到@Import注解中,我们会发现文档中写了@Import可以导入@Configuration(配置类)、ImportSelector、ImportBeanDefinitionRegistrar和常规的组件。用法详见下面。
- 通过class导入,@Import(要导入容器的组件),容器会自动注册这个组件,id默认是全类名:
@Import({DogPojo.class})//只要在这里写上类名,都会被加载到容器中
@Configuration(proxyBeanMethods = true)//告诉SpringBoot,这是一个配置类=====相当于SSM框架时代的配置文件
public class MyConfig {
}
给容器中自动创建出这两个类型(通过无参构造)的组件、默认组件的名字就是全类名
这样,就会向容器中注入DogPojo组件。当然啦,被Import修饰的类也要能作为组件扫描到Spring容器中去。
- ImportSelector:通过全类名导入,ImportSelector的selectImports方法返回所有需要导入到容器的全类名数组
自定义一个类实现ImportSelector接口
public class MyImportSelector implements ImportSelector {
//AnnotationMetadata包含了所有@Import("MyImportSelector.class")修饰的类的所有信息
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
"com.tellhow.pojo.People"};//这里的全类名会加到组件中
}
@Override
public Predicate<String> getExclusionFilter() {
return ImportSelector.super.getExclusionFilter();
}
}
- ImportBeanDefinitionRegistrar的registerBeanDefinitions方法直接注册到容器中。
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
*
* @param importingClassMetadata 当前类的所有信息
* @param registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean myDog = registry.containsBeanDefinition("myDog");
if (myDog){
//"book":组件名,后面的对象是组件的定义信息
registry.registerBeanDefinition("book", new RootBeanDefinition(BookPojo.class) );
}
}
}
4、原生配置文件引入
@ImportResource
原生的bean.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="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.2.xsd">
<bean name="dog02" class="com.tellhow.pojo.DogPojo">
<property name="age" value="12"></property>
<property name="name" value="花花"></property>
<property name="toll" ref="mytool02"></property>
</bean>
<bean name="mytool02" class="com.tellhow.pojo.ToolPojo">
<property name="toolName" value="铲子"></property>
<property name="toolNo" value="123456789"></property>
</bean>
</beans>
@ImportResource注解用于导入Spring的配置文件,让配置文件(xml)里面的内容生效;(就是以前写的springmvc.xml、applicationContext.xml)
Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,它也不能自动识别;
想让Spring的配置文件(xml)生效,加载到容器;@ImportResource标注在一个配置类上,将配置文件的路径引进来。
@ImportResource(locations="classpath:bean.xml")
@Configuration
public class MyConfig02 {
}
经过上面这两步,bean.xml下的组件就会被加载到容器中。
2.2 条件装配@Conditional
条件装配:在满足Conditional指定的条件,才会将组件加到容器中,下面的Conditional接口的一些实现类。
条件装配测试实例
这里的 @ConditionalOnBean其实与加载的顺序也有关系,就像下面这个,ToolPojo放DogPojo的前面,DogPojo的条件装配条件会通过,因为在加载DogPojo时,容器中已经存在ToolPojo类型的组件了。如果他们位置发生变化,DogPojo的条件装配条件不会通过了。
@Configuration(proxyBeanMethods = true)//告诉SpringBoot,这是一个配置类=====相当于SSM框架时代的配置文件
public class MyConfig {
@Bean
public ToolPojo myTool() {
return new ToolPojo("锤子", 1008612L);
}
@Bean//给容器添加组件,返回类型是组件类型,方法名默认首子母小写是组件名,返回值就是组件在容器中的实例
@ConditionalOnBean(value = {
ToolPojo.class})//容器中存在ToolPojo类型的组件,myDog才会注册到容器中。
public DogPojo myDog() {
DogPojo dogPojo = new DogPojo();
return dogPojo;
}
}
2.3配置绑定
在SSM时代,想将.properties文件与javaBean绑定。需要先用IO流将配置文件读取到内存中,然后在解析读取到的配置文件内容绑定到JavaBean上
public static void main(String[] args) {
SpringApplication.run(JkczpApplication.class, args);
//printBeanmame(applicationContext);
Properties properties = new Properties();
FileInputStream fileInputStream = null;
URL url = JkczpApplication.class.getClassLoader().getResource("config.properties");
try {
fileInputStream = new FileInputStream(url.getFile());
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
properties.load(inputStreamReader);
Enumeration enumeration = properties.propertyNames();
while (enumeration.hasMoreElements()){
String key = (String)enumeration.nextElement();
System.out.println("key为"+key+",值为"+properties.getProperty(key));
}
} catch (Exception e) {
e.printStackTrace();
}
SpringBoot使用@ConfigurationProperties+@Component将配置文件与JavaBean绑定。默认绑定的是application.properties和application.yml配置文件。(需要被组件扫描器,扫描到组件中哦)
3、@EnableConfigurationProperties +@ConfigurationProperties
@Configuration(proxyBeanMethods = true)//告诉SpringBoot,这是一个配置类=====相当于SSM框架时代的配置文件
//1、开启CatProperties配置绑定功能
//2、把这个CatProperties这个组件自动注册到容器中
@EnableConfigurationProperties(value = CatProperties.class)//与Configuration一起,不然不会生效
public class MyConfig {
}
@ConfigurationProperties("mycat")//配置绑定,但是这个没加入到容器中不会生效。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CatProperties {
private String catName;
private String catSex;
private Integer catAge;
}
3、自动配置原理入门
3.1、引导加载自动配置类
被@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) })
1、@SpringBootConfiguration。它也是一个复合注解(元注解我先去掉了)
@Configuration
@Indexed
public @interface SpringBootConfiguration {
}
@Configuration,被这个注解标注的类为配置类,所以主程序类也是一个配置类。
@Indexed:在项目中使用了@Indexed之后,编译打包的时候会在项目中自动生成META-INT/spring.components文件。当Spring应用上下文执行
ComponentScan扫描时,META-INT/spring.components将会被CandidateComponentsIndexLoader 读取并加载,转换为CandidateComponentsIndex对象,这样的话
@ComponentScan不在扫描指定的package,而是读取CandidateComponentsIndex对象,从而达到提升性能的目的。
2、@EnableAutoConfiguration。它也是一个复合注解,也是最重要的注解。
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
- 其中@AutoConfigurationPackage也是一个复合注解:主要功能就是将AutoConfigurationPackages组件注册到容器中
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
//前面讲过import到组件的三种方式,这个是属于ImportBeanDefinitionRegistrar导入方式(直接注册bean的定义信息),因为Registrar 是内部类
/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
上面做的事,就是向容器注入一个组件,组件名为org.springframework.boot.autoconfigure.AutoConfigurationPackages,组件为new BasePackagesBeanDefinition(packageNames));//packageNames是主程序所在包信息。
关于自动配置包,注解给出的解释是Registers packages with AutoConfigurationPackages. When no base packages or base package classes are specified, the package of the annotated class is registered。(使用 AutoConfigurationPackages 注册包。当没有指定基包或基包类时,注解类的包被注册。)
总结:@Import(AutoConfigurationPackages.Registrar.class)就是向容器中注册了一个,有关主程序类所在包信息的组件。
- @Import(AutoConfigurationImportSelector.class)这个是SpringBoot自动配置的核心
这不用说了,又是在给容器导入组件。还是以全类名的方式导入的。让我们点进去瞧瞧吧。
/**
*AnnotationMetadata annotationMetadata 注解的元信息
* return 所有加入到容器中的组件全类名信息
**/
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//获取自动配置实体
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
//从 AnnotationMetadata 返回适当的 AnnotationAttributes。默认情况下,此方法将返回 getAnnotationClass() 的属性。
//获取属性信息
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取候选的配置
List<String> configurations = getCandidateConfigurations(ann