什么是SpringBoot自动配置
在没有SpringBoot情况下,我们引入第三方的依赖之后,需要手动配置,比如需要手动将引入的第三方依赖通过xml文件或注解的方式注入到 Ioc 容器中。并可能需要对注入到Ioc容器中的bean进行一些配置,非常麻烦。但是,在Spring Boot 中,我们直接引入一个 starter 即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
其实SpringBoot定义了一套接口规范,SpringBoot在启动时会扫描外部引用的jar包中的 META-INF/spring.factories 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装配进 SpringBoot。
引入starter之后,我们只需要通过少量的注解和一些简单的配置即可使用。
SpringBoot是如何实现自动配置的
在探究自动配置原理之前,我们需要先了解几个重要的核心注解。
Condition注解
Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean 操作。所有的Condition注解都实现了Condition接口
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
在这个接口中的matches方法返回一个boolean值,相关的注解根据这个值做出对应操作
matches 方法两个参数:
• context:上下文对象,可以获取属性值,获取类加载器,获取BeanFactory等。
• metadata:元数据对象,用于获取注解属性。
Condition注解基础用法:
需求:导入jedis的坐标后创建User的Bean对象
1. 新建一个SpringBoot项目,导入redis环境和jedis坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
创建一个User类。
一个自定义ClassCondition类并且实现Condition接口
一个配置类------->用于使用自定义规则的注解
项目结构如下:
package com.ape.springboot_condition_01.domain;
public class User {
}
package com.ape.springboot_condition_01.condition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@ConditionalOnBean
public class ClassCondition implements Condition {
/**
*
* @param context:上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata:注解元对象。 可以用于获取注解定义的属性值
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//1.需求: 导入Jedis坐标后创建Bean
boolean flag = true;
try {
Class<?> aClass = Class.forName("redis.clients.jedis.Jedis");
} catch (ClassNotFoundException e) {
e.printStackTrace();
flag = false;
}
return flag;
}
}
package com.ape.springboot_condition_01.config;
import com.ape.springboot_condition_01.condition.ClassCondition;
import com.ape.springboot_condition_01.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserConfig {
//@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
@Bean
@Conditional(value = ClassCondition.class)
public User user(){
return new User();
}
}
测试:
package com.ape.springboot_condition_01;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class SpringbootCondition01Application {
public static void main(String[] args) {
//springboot的ioc容器
ConfigurableApplicationContext run = SpringApplication.run(SpringbootCondition01Application.class, args);
Object user = run.getBean("user");
System.out.println(user);
}
}
情况1: 没有添加坐标,发现为空(将配置文件jedis坐标注释或删除)
情况2 :有添加坐标,发现有对象(恢复jedis坐标)
SpringBoot 提供的常用条件注解:这些注解在springBoot-autoconfigure的condition包下
- ConditionalOnProperty:判断配置文件中是否有对应属性和值才初始化Bean
- ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean
- ConditionalOnMissingBean:判断环境中没有对应Bean才初始化Bean
- ConditionalOnBean:判断环境中有对应Bean才初始化Bean
Enable注解
SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用@Import注解导入一些配置类,实现Bean的动态加载。
Enable基础用法:
@Enable底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中。而@Import提供4中用法:
① 导入Bean
② 导入配置类
③ 导入 ImportSelector 实现类。一般用于加载配置文件中的类
④ 导入 ImportBeanDefinitionRegistrar 实现类。
@Import的①②用法非常简单,添加注解传入要导入配置类字节码即可
@Import(User.class) //导入javaBean
@Import(UserConfig.class) //导入配置类
重点说③ ④ 用法
方式③ :ImportSelector 接口,中有一个方法selectImports,返回值类型是一个String类型数组,这个数组中存储的就是被加载进容器的配置类路径。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.context.annotation;
import java.util.function.Predicate;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
我们可以自定义一个类实现ImportSelector 接口,
package com.ape.springboot_unable_04.config;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//目前字符串数组的内容是写死的,未来可以设置在配置文件中动态加载
return new String[]{"com.ape.springboot_unable_04.domain.User","com.ape.springboot_unable_04.domain.User"};
}
}
测试(当前未给容器中注入User对象)
@SpringBootApplication
@Import(MyImportSelector.class)
public class SpringbootUnable04Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootUnable04Application.class, args);
User user = run.getBean(User.class);
System.out.println(user);
}
}
测试结果------->User对象被注入成功
方式④ImportBeanDefinitionRegistrar接口
自定义接口实现ImportBeanDefinitionRegistrar
package com.ape.springboot_unable_04.config;
import com.ape.springboot_unable_04.domain.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 {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//AnnotationMetadata注解
//BeanDefinitionRegistry向spring容器中注入
//1.获取user的definition对象
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
//2.通过beanDefinition属性信息,向spring容器中注册id为user的对象
registry.registerBeanDefinition("user", beanDefinition);
}
}
测试
package com.ape.springboot_unable_04;
import com.ape.springboot_unable_04.config.MyImportBeanDefinitionRegistrar;
import com.ape.springboot_unable_04.config.MyImportSelector;
import com.ape.springboot_unable_04.domain.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootUnable04Application {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootUnable04Application.class, args);
User user = (User) run.getBean("user");
System.out.println(user);
}
}
测试结果 注入成功
SpringBooot自动配置原理
首先我们从SpringBoot项目的核心注解@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 {
... ...
}
@SpringBootApplication内部使用了许多注解
元注解:
@Target({ElementType.TYPE}) --------用来限制注解的使用范围
@Retention(RetentionPolicy.RUNTIME) --------- 保留策略
@Documented -------一个标记注解,用于指示一个注解将被文档化
@Inherited ---------使父类注解能被子类继承
跟进@SpringBootConfiguration
发现@SpringBootConfiguration其实还是使用的@Configuration(允许在上下文中注册额外的 bean 或导入其他配置类,作用与 applicationContext.xml 的功能相同),做了包装而已。
@ComponentScan: 扫描包下的类中添加了@Component (@Service,@Controller,@Repostory,@RestController)注解的类 ,并添加的到spring的容器中,可以自定义不扫描某些 bean。
所以说 @SpringBootApplication 是一个复合注解,大概就可以把 @SpringBootApplication 看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。
也就是说,@EnableAutoConfiguration 是实现自动装配的核心注解
跟进内部如下
抛开元注解,剩下
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
跟进@AutoConfigurationPackage内部
发现导入了一个注册类,继续跟进注册类
该注册类实现了ImportBeanDefinitionRegistrar接口,还记得吗上文提到的@Import的第④种用法
继续跟进另外一个 @Import({AutoConfigurationImportSelector.class})
回想@Import的第③种用法,string数组种存储的是所有要导入的配置类
阅读源码发现,string数组被getAutoConfigurationEntry方法提供,继续跟进该方法。
又被getCandidateConfigurations提供,继续跟进
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;
}
跟到这里其实已经足够了,阅读代码,注意到断言抛出异常信息,没有自动配置类被发现在META-INF/spring.factories.
也就是说来源在这个文件。接下来我们去找一下这个文件位置,
打开项目的外部导入库
通过查阅发现并不是所有都有这个spring.factories文件
只有后缀为autoconfigure的库下才有spring.factories文件
打开文件我们发现,项目中使用的所有基础依赖的配置类都在这里以全限定类名形式保存。
但是项目启动时这些配置都会全部加载吗,我们跟进一个常用的配置
发现被Condition注解修饰,也就是说动态根据条件加载。
到此SpringBoot的自动配置原理以及非常清晰了,
- @EnableAutoConfiguration 注解内部使用 @Import(AutoConfigurationImportSelector.class)来加载配置类。
- 配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot应用启动时,会自动加载这些配置类,初始化Bean
- 并不是所有的Bean都会被初始化,在配置类中使用Condition来加载满足条件的Bean
自定义启动器
了解过SpringBoot的自动配置原理后我们就可以尝试自定义启动器
首先创建三个SpringBoot项目
需求:在springboot_autoconfigure中导入jedis依赖启动器导入该项目,测试项目导入启动器,jedis对象注入成功则成立
1.springboot_autoconfigure 用来模拟启动器下的多个依赖
pom.xml中引入jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
写一个配置类,向容器中注入一个jedis对象
package com.ape.springboot_autoconfigure;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
@Configuration
public class RedisAutoConfiguration {
//注入jedis
@Bean
public Jedis jedis(){
return new Jedis();
}
}
2.springboot_starter 用来模拟启动器
pom.xml中导入springboot_autoconfigure坐标(模拟启动器可以有多个)
<!--模拟启动器中引入 其他模块-->
<dependency>
<groupId>com.ape</groupId>
<artifactId>springboot_autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--
......
多个
-->
3.springboot_test 测试类
pom.xml中引入启动器
<!--模拟引入启动器-->
<dependency>
<groupId>com.ape</groupId>
<artifactId>springboot_starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
测试
package com.ape.springboot_test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import redis.clients.jedis.Jedis;
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SpringbootTestApplication.class, args);
Jedis jedis = run.getBean(Jedis.class);
System.out.println(jedis);
}
}
测试结果
jedis对象注入成功