Spring boot 自动装配
文章目录
自动装配是什么?
问题:我们在导入启动坐标以后,为什么可以用@Autowired在spring容器中获取对应的类呢?
在学习Spring初期,我们发现我们使用autowried都是手动注入容器的,比如在配置文件中<bean>
或者@Service、@controller、@Component,还有在配置类中@bean来注入容器的。
那为什么我在pom.xml文件中导入坐标,没有以上操作,却可以@autowried获取对应的实体类呢?
例如,导入Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
测试类
@SpringBootTest
class SpringbootCondition01ApplicationTests {
@Autowired
RedisTemplate redisTemplate;
@Test
void contextLoads() {
System.out.println(redisTemplate);
}
}
结果
org.springframework.data.redis.core.RedisTemplate@4ed15347
这就是SpringBoot的自动装配。他的原理是什么?探索原理前我们需要知道以下技能。
技能一:condition
功能:condition是一个条件判断功能,可以选择性的创建bean
建立一个user类
public class User {
}
有一个需求:如果项目导入Jedis的坐标,才给ioc容器中注入user对象
问题来了,SpringBoot如何知道导入了Jedis而实例化user呢?
如果直接写一个配置类
@Configuration
public class UserConfig {
@Bean
public User user(){
return new User();
}
}
那么项目启动时,user类则会自动注入容器。不符合我们的需求。这时需要使用condition注解
Conditional注解源码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
这个注解中只声明了一个value的数组,注意泛型:Condition的实现类
Condition源码:
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
这个接口只定义了一个matches方法,返回值是一个Boolean类型。
这个方法返回值如果是true则执行注解的方法,如果为false则不执行注解的方法
实现一个condition接口的类
public class ClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
可以看到默认返回值类型为false,所以我们可以给这个类改造一下
public class UserCondition implements Condition {
/**
* @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
* @param metadata 注解元对象。 可以用于获取注解定义的属性值
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 定义一个布尔值
boolean flag = true;
try {
// 通过反射的方法,找到Jedis的驱动
Class<?> aClass = Class.forName("redis.clients.jedis.Jedis");
// 如果找到默认flag为true
} catch (ClassNotFoundException e) {
// 如果没有找到,则flag为false
flag=false;
}
// 最终返回这个Boolean值
return flag;
}
}
这样我们可以在我们的user类注入方法上,使用conditional的注解,用来判断如果Jedis导入了就注入user,如果没有导入则不执行,不注入。
@Configuration
public class UserConfig {
//@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
@Bean
@Conditional(value = UserCondition.class)
public User user(){
return new User();
}
}
测试:
启动类
@SpringBootApplication
public class SpringbootConditionApplication {
public static void main(String[] args) {
//启动SpringBoot的应用,返回Spring的IOC容器
ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringbootCondition01Application.class, args);
Object user = applicationContext.getBean("user");
System.out.println(user);
}
}
pom.xml中导入Jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
结果
com.ape.springboot_condition_01.domain.User@44924587
pom.xml中注掉Jedis
<!-- <dependency>-->
<!-- <groupId>redis.clients</groupId>-->
<!-- <artifactId>jedis</artifactId>-->
<!-- </dependency>-->
结果
org.springframework.beans.factory.NoSuchBeanDefinitionException
至此完成–加载Jedis就注入user,未加载就不注入
总结:
自定义conditional注解使用方法:
1.自定义类实现condition接口,重写matches方法,在matches中进行逻辑判断。
2.在初始化bean时,使用@conditional(自定义condition类.class)
注解,可以实现符合逻辑则注入,不符合时不注入。
condition中有很多子注解:
常用的注解:
ConditionalOnProperty:判断配置文件中是否有对应属性和值才初始化Bean
ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean
ConditionalOnMissingBean:判断环境中没有对应Bean才初始化Bean
ConditionalOnBean:判断环境中有对应Bean才初始化Bean
技能二:Enable
问题:通过上述condition,我们可以完成选择性注入ioc了,但是我们还是需要写一个配置类,然后在配置类上添加condition注解,也不是自动的,怎么样才能完成自动的选择性注入ioc呢?
直接来看Enable相关源码:
例如:@EnableAsync:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
Class<? extends Annotation> annotation() default Annotation.class;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
例如:@EnableScheduling:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}
我们可以观察到,每一类Enable注解的背后,都是@import导入一个类
所以我们可以将上面的condition的配置信息写入一个单独的项目。
我们导入项目,使用自定义的EnableUser来实现导入
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Import(UserConfig.class)
public @interface EnableUser {
}
在一个全新的项目中导入上一个项目,使用EnableUser注解来实现自动加载
@SpringBootApplication
@EnableUser// 自定义的Enable
public class SpringbootEnableApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringbootEnable01Application.class, args);
//获取Bean
User user = applicationContext.getBean(User.class);
System.out.println(user);
}
}
结果
com.ape.springboot_enable_02.domain.User@648ee871
至此已经初具雏形了,完了自动装配的基本原理。
技能三:import
import的用法:
- 导入Bean
- 导入配置类
- 导入ImportSelector的实现类
- 导入ImportBeanDefinitionRegistrar实现类
import源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
Class<?>[] value();
}
核心是一个value()的class数组
1、导入bean
导入普通类、实体类,会自动将导入的类注入容器
@Import(User.class)
2、导入配置类
可以在另一个配置类中,使用此配置类中的配置信息,如@bean等
@Import(UserConfig.class)
3、导入ImportSelector的实现类
importSelector源码
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
如果@Import引入一个ImportSelector的实现类,代表将“字符串数组”中的的类,全部导入spring容器
来尝试自定义一个ImportSelector的实现类
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//目前字符串数组的内容是写死的,未来可以设置在配置文件中动态加载
return new String[]{"com.apesource.domain.User", "com.apesource.domain.Student"};
}
}
两个实体类
public class Student {
}
public class User {
}
在启动类中使用import注解导入MyImportSelector类
@SpringBootApplication
@Import(MyImportSelector.class)
public class SpringbootEnable03Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnable03Application.class, args);
User user = context.getBean(User.class);
System.out.println(user);
Student student = context.getBean(Student.class);
System.out.println(student);
}
}
结果
com.ape.springboot_import_02.domain.User@41477a6d
com.ape.springboot_import_02.domain.Student@6fe46b62
4、导入ImportBeanDefinitionRegistrar实现类
这个接口提供了通过spring容器api的方式直接向容器中注册bean
ImportBeanDefinitionRegistrar源码:
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
自定义一个类MyImportBeanDefinitionRegistrar实现ImportBeanDefinitionRegistrar接口
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);
}
}
在启动类中使用import注解导入MyImportBeanDefinitionRegistrar类
@SpringBootApplication
@Import({MyImportBeanDefinitionRegistrar.class})
public class SpringbootEnable03Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnable03Application.class, args);
User user = (User) context.getBean("user");
System.out.println(user);
}
}
结果
com.ape.springboot_import_02.domain.User@41477a6d
Boos:启动类上的@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}
)}
)
public @interface SpringBootApplication {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
重点:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
@SpringBootConfiguration
源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
这个源码中只有一个@Configuration,一个声明配置类,所以我们的启动类也是一个配置类,可以在启动类中@Bean
@ComponentScan
作用:
扫描文件的,这个扫描的范围是启动类
的同级路径及子路径,扫描到特定的@Component、@Service、@Controler、@Repository、@Configuration等等注解后,会做相应的bean注册和配置文件bean注册工作。
@EnableAutoConfiguration
源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
Enable注解,技能二的介绍的就是用来自动装配的,他的内部就是import注解 @Import({AutoConfigurationImportSelector.class})
AutoConfigurationImportSelector一看名字,就是import技能三的第三种用法。
必然重写selectImports方法,返回一个class类型的数组。
AutoConfigurationImportSelector类中的selectImports()方法源码:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
里面太多了,其他省略只找重要的selectImports方法,此方法一定要返回一个String[]数组,此数组中包括是每个类的完全名称的列表,这样返回给import的时候,才会将类注入Spring容器
if中的返回值NO_IMPORTS,就是一个空数组,不符合
else中返回的StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
autoConfigurationEntry是通过getAutoConfigurationEntry得到的一个map的Entry对象
getAutoConfigurationEntry()方法源码
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
此方法中返回了一个new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
configurations(配置信息)和exclusions(排除在外的,不需要的)
向上找configurations相关
List
<String>
configurations = this.getCandidateConfigurations(annotationMetadata, attributes);所以SpringBoot自动配置的配置信息就是从getCandidateConfigurations()方法来的
getCandidateConfigurations()方法源码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
“No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.”
这行解释中,META-INF/spring.factories 就是存放配置信息的关键点
找到META-INF/spring.factories 文件位置
找一个熟悉的配置类,我们进去看看
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
RedisAutoConfiguration源码
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public 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;
}
}
RedisTemplate()和stringRedisTemplate()方法上都有一个ConditionalOnMissingBean,技能一的子注解!!
还是选择性的加载,如果容器中有就不加载,如果没有则加载
以上就是自动装配的过程
打个断点调试一下:
我们可以看到这个configurations 的list集合中,就是所有配置类的完全限定名称
将这些配置类全放在import中,依次加载,再根据condition注解,进行选择性注入。这就是SpringBoot的自动装配
所以SpringBoot的自动装配实质就是,SpringBoot已经帮助我们写了配置类,都做了选择性装配。我们只需要启动项目,就可以完成自动装配。
自动装配的原理即为:
启动类@SpringBootApplication
里面有一个@EnableAutoConfiguration – 自动装配开始
Enable的内部就是@Import({AutoConfigurationImportSelector.class}) – 技能三中的第三种方法
ImportSelector的实现类AutoConfigurationImportSelector类 – 实现了ImportSelector接口
重写了selectImports()方法 – 此方法需要返回一个String[]数组
方法中就是用getAutoConfigurationEntry(annotationMetadata)方法返回数组 – 最终是由此方法的enrty类返回数组
获取这个class数组的原理就是List
<String>
configurations = this.getCandidateConfigurations(annotationMetadata, attributes); – 用来获取所有自动装配的文件中的配置类信息
读取META-INF/spring.factories中的key=EnableAutoConfiguration的所有值 – 存放配置类信息的地方
一步一步返回到import去加载对应的配置类 – import逐一进行选择性注入Spring容器
在配置类中根据condition完成选择注入Spring容器。即自动装配
注意:在SpringBoot2.7版本后,配置信息的位置有些小改变
No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
文件位置虽然改变了,但是配置类的信息还是一致的。
总结:
springboot的自动装配,是通过import导入自己在META-INF/spring.factories
或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
中写好的配置类信息,然后使用condition选择性加载Bean的。
自动的装配的原理,就是写好配置类,将配置类的完全路径写入META-INF/spring.factories中的EnableAutoConfiguration键下面,就可以完成自动的装配了。
但是Springboot只提供了100多个自动装配的对象,那springboot没有提供的配置类,又是如何自动装配呢?
第三方的配置类,一般会自己提供自己的配置类,写在META-INF/spring.factories中的EnableAutoConfiguration的下面,SpringBoot都会扫描到配置类,完成自动配置
如果没有配置类,我们还是需要手动将第三方技术注入Spring容器中。