Spring Boot自动配置原理

一、什么是自动配置?

自动配置是指在应用程序启动时,SpringBoot根据classpath类路径下的依赖自动应用配置程序所需的一系列bean和配置类,从而减少开发者的配置工作,提高开发效率。

二、Condition

Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean 操作。

     Condition 是一个接口,只有一个 matches 方法,返回 true 则表示条件匹配。matches 方法的两个参数分别是上下文信息和注解的元信息,从这两个参数中可以获取到 IOC 容器和当前组件的信息,从而判断条件是否匹配。

源码分析:

@Conditional是条件注解中的属性 value,其类型是 Condition 数组。组件必须匹配数组中所有的 Condition,才可以被注册。

源码分析:

案例:

需求1 在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求:

  1. 导入Jedis坐标后,加载该Bean,没导入,则不加载。
XML 复制代码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第一步:准备一个User实体类

java 复制代码
public class User {
}

第二步:编写UserConfig配置类

  • @Conditional(value= ClassCondition.class)是条件注解,它指定了一个条件类ClassCondition。Spring在创建user这个Bean之前,会先检查ClassCondition类中的matches方法是否返回true
  • 如果matches方法返回true,那么Spring将创建并注册user这个Bean;如果返回false,则不会创建这个Bean。
java 复制代码
@Configuration
public class UserConfig {
    //@Conditional中的ClassCondition.class的matches方法,返回true执行以下代码,否则反之
    @Bean
    @Conditional(value= ClassCondition.class)
    public User user(){
        return new User();
    }
}

第三步:定义了一个名为ClassCondition的类,它实现了Spring框架中的Condition接口。ClassCondition的目的是作为一个条件判断器,用于决定某个Bean是否应该被创建

matches方法是 Condition 接口中的唯一方法。它的返回值决定了某些配置或 Bean 是否应该被加载。ConditionContext context:提供了上下文对象,可以用来获取 Spring 环境、IOC 容器、ClassLoader 等。AnnotatedTypeMetadata metadata:提供了注解的元数据,可以用来获取注解的属性值。

java 复制代码
public class ClassCondition implements Condition {
    /**
     *
     * @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
     * @param metadata 注解元对象。 可以用于获取注解定义的属性值
     * @return
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //1.需求: 导入Jedis坐标后创建Bean
        //思路:判断redis.clients.jedis.Jedis.class文件是否存在

        boolean flag = true;
        try {
            Class<?> cls = Class.forName("redis.clients.jedis.Jedis");
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return flag;

    }
}

第四步:启动SpringBoot的应用,返回Spring的IOC容器,获取Bean,redisTemplate

  • 情况1 没有添加坐标前,发现为空
  • 情况2 有添加坐标前,发现有对象
java 复制代码
@SpringBootApplication
public class SpringbootCondition01Application {

    public static void main(String[] args) {

        //启动SpringBoot的应用,返回Spring的IOC容器
        ConfigurableApplicationContext context =  SpringApplication.run(SpringbootCondition01Application.class, args);
        //获取Bean,redisTemplate
        //情况1 没有添加坐标前,发现为空
        //情况2 有添加坐标前,发现有对象
//        Object redisTemplate = context.getBean("redisTemplate");
//        System.out.println(redisTemplate);

        /********************案例1********************/
        Object user = context.getBean("user");
        System.out.println(user);


    }

}

观察结果,添加了jedis坐标控制台会输出User对象的内存地址,如果没有添加jedis的坐标会报异常NoSuchBeanDefinitionException

需求2 在 Spring 的 IOC 容器中有一个 User 的 Bean,现要求:

将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定

实现步骤:

  • 不使用@Conditional(ClassCondition.class)注解
  • 自定义注解@ConditionOnClass,因为他和之前@Conditional注解功能一直,所以直接复制
  • 编写ClassCondition中的matches方法

第一步:准备实体类User

java 复制代码
public class User {
}

第二步:编写UserConfig配置类

java 复制代码
@Configuration
public class UserConfig {

    //情况1
    @Bean
//    @Conditional(ClassCondition.class)
//    @ConditionOnClass(value = "redis.clients.jedis.Jedis")
    @ConditionOnClass(value={"com.alibaba.fastjson.JSON","redis.clients.jedis.Jedis"})
    public User user(){
        return new User();
    }

第三步:定义了一个名为ClassCondition的类,它实现了Spring框架中的Condition接口。ClassCondition的目的是作为一个条件判断器,用于决定某个Bean是否应该被创建

java 复制代码
public class ClassCondition  implements Condition {
    /**
     *
     * @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
     * @param metadata 注解元对象。 可以用于获取注解定义的属性值
     * @return
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //1.需求: 导入Jedis坐标后创建Bean
        //思路:判断redis.clients.jedis.Jedis.class文件是否存在
//        boolean flag=true;
//        try {
//            Class<?> cls=Class.forName("redis.clients.jedis.Jedis");
//        } catch (ClassNotFoundException e) {
//            flag=false;
//        }
//        return flag;

上述代码首先实现了Condition接口,Condition 接口用于定义条件,Spring 在决定是否加载某些配置或 Bean 时会使用它。这也就是上面的问题的解决,通过条件来判断是否创建bean。

matches 方法是 Condition 接口中的唯一方法。它的返回值决定了某些配置或 Bean 是否应该被加载。ConditionContext context:提供了上下文对象,可以用来获取 Spring 环境、IOC 容器、ClassLoader 等。AnnotatedTypeMetadata metadata:提供了注解的元数据,可以用来获取注解的属性值。

首先通过getAnnotationAttributes方法获取ConditionOnClass注解的属性值,这里的ConditionOnClass是我们自定义的注解,等会我们来讲。

通过map集合通过get方法获取到了里面的"com.alibaba.fastjson.JSON", "redis.clients.jedis.Jedis"两个依赖。

通过遍历里面的类名,并尝试去加载这些类,创建这些类的对象。如果能加载成功,则保持返回true,如果不能成功加载,则抛出异常并将flag设置为false返回给ConditionOnClass自定义注解。

然后我们要去创建一个自定义注解,因为你不能每次去判断某个类是否依赖都去在条件类里卖弄重新写入,所以通过一个自定义注解来接收测试类中传进来的依赖类来完成判断,下面是自定义注解ConditionOnClass的代码:

第四步:自定义一个注解类ConditionOnClass是一个自定义的注解,它结合了Spring的条件化配置机制。通过这个注解,你可以轻松地实现基于类路径中存在特定类来条件化地创建Bean的功能。

java 复制代码
// 指定了这个注解可以应用的目标。ElementType.TYPE 表示注解可以用在类、接口或枚举上,ElementType.METHOD 表示注解可以用在方法上。
@Target({ElementType.TYPE,ElementType.METHOD}) // 可以修饰在类与方法上
// 指定了注解的保留策略。RUNTIME 表示注解会在运行时保留,可以通过反射机制读取。
@Retention(RetentionPolicy.RUNTIME) // 注解生效节点runtime
@Documented //生成工具文档化
// 表示这个注解的作用是条件性的,只有当 ClassCondition 条件满足时,注解修饰的类或方法才会生效。ClassCondition 是一个实现了 Condition 接口的类,用来定义条件逻辑。
@Conditional(value = ClassCondition.class)
public @interface ConditionOnClass {
    String[] value(); // 设置此注解的属性redis.clients.jedis.Jedis
}

第五步:启动SpringBoot的应用,返回Spring的IOC容器

java 复制代码
@SpringBootApplication
public class SpringbootCondition02Application {

    public static void main(String[] args) {
        //启动SpringBoot的应用,返回Spring的IOC容器
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootCondition02Application.class, args);

        /********************获取容器中user********************/
       Object user1 = context.getBean("user");
       System.out.println(user1);

        // Object user2=context.getBean("user2");
        // System.out.println(user2);
    }

}

输出结果:

观察结果,添加了jedis坐标控制台会输出User对象的内存地址,如果没有添加jedis的坐标会报异常NoSuchBeanDefinitionException

需求3 导入通过注解属性值value指定坐标后创建Bean

java 复制代码
//2.需求: 导入通过注解属性值value指定坐标后创建Bean
        //获取注解属性值  value
        Map<String,Object> map=metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
        System.out.println(map);
        String[] value=(String[]) map.get("value");

        boolean flag=true;
            try {
                for(String className:value) {
                    Class<?> cls = Class.forName(className);
                }
            } catch (ClassNotFoundException e) {
                flag=false;
            }
            return flag;
        }

    }
java 复制代码
//情况2
    @Bean
    //当容器中有一个key=k1且value=v1的时候user2才会注入
    //在application.properties文件中添加k1=v1
    @ConditionalOnProperty(name="k1",havingValue = "v1")
    public User user2(){
        return new User();
    }
}
java 复制代码
@SpringBootApplication
public class SpringbootCondition02Application {

    public static void main(String[] args) {
        //启动SpringBoot的应用,返回Spring的IOC容器
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootCondition02Application.class, args);

        /********************获取容器中user********************/
//        Object user1 = context.getBean("user");
//        System.out.println(user1);

        Object user2=context.getBean("user2");
        System.out.println(user2);
    }

}

输出结果:

同上如果添加了jedis坐标控制台会输出User对象的内存地址,如果没有添加jedis的坐标会报异常NoSuchBeanDefinitionException

Condition -- 小结

自定义条件:

①定义条件类:自定义类实现Condition接口,重写matches方法,在matches方法中进行逻辑判断,返回

  • boolean值。matches方法两个参数:
    • context: 上下文对象,可以获取属性值,获取类加载器,获取BeanFactory等。
    • metadata:元数据对象,用于获取注解属性。

②判断条件:在初始化Bean时,使用@Conditional(条件类.class)注解

SpringBoot提供的常用条件注解:

一下注解在springBoot-autoconfigure的condition包下

  • ConditionalOnProperty:判断配置文件中是否有对应属性和值才初始化Bean
  • ConditionalOnClass:判断环境中是否有对应字节码文件才初始化Bean
  • ConditionalOnMissingBean:判断环境中没有对应Bean才初始化Bean
  • ConditionalOnBean:判断环境中有对应Bean才初始化Bean

可以查看RedisAutoConfiguration类说明以上注解使用

距离演示ConditionalOnProperty

二、@Enable注解

@Enable被大量用于进行启动某些功能,其底层使用的就是@import注解。导入一些配置类,实现Bean的动态加载。

@Import注解

@Enable底层依赖于@Import注解导入一些类,使用@Import导入的类会被Spring加载到IOC容器中。

而@Import提供4中用法:

① 导入Bean

② 导入配置类

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

④ 导入 ImportBeanDefinitionRegistrar 实现类。

三、Spring Boot是如何实现自动装配?

Spring Boot实现自动装配是通过SpringBoot项目启动类上的**@SpringBootApplication**注解来实现。

Spring Boot的自动装配实际上是从 META-INF/spring.factories 文件中获取到对应的需要进行自动装配的类,并生成相应的Bean对象,然后将它们交给Spring容器进行管理。

    如果没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置。

@SpringBootApplication注解源码分析:

观察@SpringBootApplication源码可知,@SpringBootApplication主要由 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan这三个注解组成。

  • @SpringBootConfiguration:标注在某个类上,表示这是一个Spring Boot的配置类;
  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @ComponentScan:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中

其中 @EnableAutoConfiguration 是实现自动装配的主要注解。下面我们来分析一下@EnableAutoConfiguration的源码

@AutoConfigurationPackage:将主程序类所在包及所有子包下的组件扫描到Spring容器中。

@Import:

    ① 导入Bean

    ② 导入配置类

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

    ④ 导入 ImportBeanDefinitionRegistrar 实现类。

观察@EnableAutoConfiguration的源码可知,实现自动配置主要是通过AutoConfigurationImportSelector,实现类来加载配置文件按的

AutoConfigurationImportSelector实现类的源码解读

观察源码可知AutoConfigurationImportSelector是 Spring Boot 自动配置机制的核心组件之一,它通过动态选择并导入自动配置类来简化 Spring 应用的配置过程 ,在AutoConfigurationImportSelector 类中我们会发现它实现了ImportSelector接口 ,也就实现了这个接口中的 selectImports 方法,该方法主要用于获取所有符合条件的类的全限定类名,并以字符串数组返回,这些类需要被加载到 IoC 容器中。

    该方法主要通过调用**getAutoConfigurationEntry()**方法获取AutoConfigurationEntry对象,这个方法主要负责加载自动配置类的。

getAutoConfigurationEntry()源码解读

观察源码可知,这个方法是 Spring Boot 自动配置机制的核心之一,它负责根据应用的配置和依赖情况来确定哪些自动配置类应该被加载到 Spring 应用上下文中。通过这个过程,Spring Boot 能够提供"开箱即用"的配置,同时允许开发者通过排除特定的自动配置来定制其行为。

List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes)就是获取到所有需要导入到容器当中的组件,利用工厂加载。

这个类实现了DeferredImportSelector等接口

在DeferredImportSelector接口中有一个selectImports,里面规定了哪些Bean需要被自动装配,根据里面的方法:this.getCandidateConfigurations方法进入

观察源码可知该类的 getCandidateConfigurations 方法中调用了 SpringFactoriesLoader类的 loadFactoryNames 方法获取所有自动转配类名,loadSpringFactories() 方法从META-INF/spring.factories加载自动装配类。该方法进入了一个META-INF/spring的目录,文件后缀是.imports

    最后按照条件装配@Conditional最终会按需配置。

    加载 spring.factories 中的配置,但不是每次启动都会加载其中的所有配置,会有一个筛选的过程,去掉重复的配置。

可以在左侧的依赖里面进行查看,这里面有133个Bean,可以被自动装配,也就是常用的自动装配的哪些Bean。

总结:

  1. Spring Boot项目中@SpringBootApplication注解实现自动装配,这个注解是对三个注解进行了封装:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,
  2. 其中@EnableAutoConfiguration是实现自动化配置的核心注解。
  3. 该注解通过@Import注解导入AutoConfigurationImportSelector,这个类实现了一个导入器接口ImportSelector。在该接口中重写了一个方法selectImports。
  4. selectImports方法的返回值是一个数组,数组中存储的就是要被导入到spring容器中的类的全限定名。在AutoConfigurationImportSelector类中重写了这个方法。
  5. 该方法内部就是读取了项目的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。
  6. 在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中

四、自定义启动器

需求: 自定义redis-starter,要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean

参考: 可以参考mybatis启动类的应用

实现步骤:

  • 创建redis-spring-boot-autoconfigure模块
  • 创建redis-spring-boot-starter模块,依赖redis-spring-boot-autoconfigure的模块
  • 在redis-spring-boot-autoconfigure模块中初始化Jedis的Bean,并定义METAINF/spring.factories文件
  • 在测试模块中引入自定义的redis-starter依赖,测试获取Jedis的Bean,操作redis。

第一步:新建一个springboot-starter-04项目,和一个redis-spring-boot-starter模块,注意记得删除redis-spring-boot-starter模块里面的启动类和配置文件,其实该模块就是一个maven项目,在springboot-starter-04项目的pom.xml文件中,添加redis-spring-boot-starter模块的坐标依赖

XML 复制代码
<!--导入坐标-->
<dependency>
      <groupId>com.ztt</groupId>
      <artifactId>redis-spring-boot-starter</artifactId>
      <version>0.0.1-SNAPSHOT</version>
</dependency>

第二步:在redis-spring-boot-autoconfigure模块中初始化Jedis的Bean,并定义META-INF/spring.factories文件

新建一个 RedisAutoconfiguration配置类,在该配置类中将jedis注入IOC容器,通过@Bean注解声明了一个Jedis类型的bean。这意味着Spring容器将管理一个Jedis实例,该实例用于与Redis服务器进行通信。

java 复制代码
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoconfiguration {
    //注入jedis
    @Bean
    public Jedis jedis(RedisProperties redisProperties){
        return new Jedis(redisProperties.getHost(),redisProperties.getPort());
    }
}

定义了一个名为RedisProperties的类,它使用了Spring Boot的@ConfigurationProperties注解来绑定application.yml文件中以spring.redis为前缀的配置属性

java 复制代码
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private String host="localhost";
    private int port=6379;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }
}

spring.factories文件

java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.ztt.RedisAutoconfiguration

第三步:在测试模块中引入自定义的redis-starter依赖,测试获取Jedis的Bean,操作redis。

测试项目springboot-starter-04中的配置文件

java 复制代码
spring:
    redis:
        port: 6060
        host: 127.0.0.1
java 复制代码
@SpringBootApplication
public class SpringbootStarter04Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootStarter04Application.class, args);
        Jedis bean1=context.getBean(Jedis.class);
        System.out.println(bean1);
    }

}

输出结果:

相关推荐
Rust研习社2 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒2 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro3 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax3 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH3 小时前
Koa和Express的区别
后端
MariaH4 小时前
Koa框架的使用
后端
luckdewei5 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某6 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy6 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom6 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github