Springboot自动装配

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的用法:

  1. 导入Bean
  2. 导入配置类
  3. 导入ImportSelector的实现类
  4. 导入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.factoriesMETA-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容器中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值