SpringBoot 自动配置之 Enable 注解原理



前言

在 SpringBoot 中提供了很多 Enable 开头的注解,这些注解都是用于动态启用某些功能的。而其底层原理是使用 @Import 注解导入一些配置类,实现 Bean 的动态加载。


@Enable* 注解

思考

SpringBoot 工程是否可以直接获取 jar 包中定义的 Bean?
答案是否定的,SpringBoot 无法直接引用别人 jar 包里的 Bean。
那么问题就来了:为什么我们之前在工程中引入一个 Redis 的起步依赖就可以直接获取到 RedisTemplate 呢?

演示

我们接下来演示一下 SpringBoot 不能获取第三方 jar 包里的 Bean 这个特点。
因为要模拟演示第三方的,我们需要创建两个模块工程:
springboot-enable、springboot-enable-other。
在这里插入图片描述
由于是演示,那么就简单写一些代码实现效果就可以。

springboot-enable-other

在 springboot-enable-other 工程中写一个 Bean:

User 类
package com.xh.config;

public class UserConfig {
}
UserConfig 配置类
package com.xh.config;

import com.xh.domain.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** 表示配置类 */
@Configuration
public class UserConfig {
    /** 注入 */
    @Bean
    public User user(){
        return new User();
    }
}
springboot-enable

在 springboot-enable 工程中依赖 springboot-enable-other 工程。
pom.xml 文件中添加代码:

        <dependency>
            <groupId>com.xh</groupId>
            <artifactId>springboot-enable-other</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

修改启动类中添加获取 User 的代码:

public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
        Object user = context.getBean("user");
        System.out.println(user);
    }

运行项目,提示没有获取到为 user 的 Bean:
在这里插入图片描述
通过演示我们可以看到 SpringBoot 不能直接获取我们在其他工程中定义的 Bean。
原因:在启动类中 @SpringBootApplication 注解中有一个 @ComponentScan 注解,这个注解扫描的范围是当前引导类所在包及其子包。
在这里插入图片描述
我们项目的引导类包路径为:om.xh.springbootenable
而 UserConfig 所在的包路径为:com.xh.config
两者并没有关联关系,那么我们如果想解决这个问题其实是有很多种方案的。

方案

1. 使用 @ComponentScan

我们可以在引导类上使用 @ComponentScan 注解扫描配置类所在的包

@SpringBootApplication
@ComponentScan("com.xh.config")
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
        Object user = context.getBean("user");
        System.out.println(user);
    }
}

启动项目,输出成功获取到的 user
在这里插入图片描述
这样的方案虽然可以解决这个问题,但是看起来还是不太行,如果我们后期需要用到第三方的一些功能,还需要把第三方的包扫描一下,如果获取的特别多的话,那么写一排实在是太 low 了!而且也很难能记住那么多的包路径,所以这种方案是不推荐的。

2.使用 @Import 注解

被 @Import 注解所导入的类,都会被 Spring 创建,并放入 IOC 容器中。
如图可以看到 @Import 注解的 value 值是一个数组,可以传多个值。
在这里插入图片描述
修改引导类

@SpringBootApplication
//@ComponentScan("com.xh.config")
@Import(UserConfig.class)
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
        Object user = context.getBean("user");
        System.out.println(user);
    }
}

启动项目,输出成功获取到的 user
在这里插入图片描述
这种方案比方案一稍微好一些,但同样会面临同样的问题,我们需要记住很多类的名字,所以仍然不是很方便。

3.对 @Import 注解进行封装

在 springboot-enable-other 工程中编写注解 @EnableUser,在注解中使用 @Import 注解导入 UserConfig,并且添加 @Import 的元注解

package com.xh.config;

import org.springframework.context.annotation.Import;
import java.lang.annotation.*;

@Import(UserConfig.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableUser {
}

修改 springboot-enable 工程的引导类,现在可以使用我们自定义的注解。

@SpringBootApplication
//@ComponentScan("com.xh.config")
//@Import(UserConfig.class)
@EnableUser
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
        Object user = context.getBean("user");
        System.out.println(user);
    }
}

启动项目,输出成功获取到的 user
在这里插入图片描述
这种自定义注解的方式和方案二是一个原理,只不过是在使用上简化了一下。

小结

SpringBoot 底层是使用 @Import 注解导入一些配置类,实现 Bean 的动态加载。
例如 @SpringBootApplication 其中的 @EnableAutoConfiguration 就使用了 @Import 来实现导入其他的类。
在这里插入图片描述

@Import 注解

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

@Import 4 种用法

@Import 提供了 4 种用法:

  1. 导入Bean
  2. 导入配置类
  3. 导入 ImportSelector 实现类。一般用于加载配置文件中的类
  4. 导入 ImportBeanDefinitionRegistrar 实现类。

1. 导入Bean

修改引导类:

/**
 * Import 4 种用法
 * 1. 导入Bean
 * 2. 导入配置类
 * 3. 导入 ImportSelector 实现类。一般用于加载配置文件中的类
 * 4. 导入 ImportBeanDefinitionRegistrar 实现类。
 */
@SpringBootApplication
//@ComponentScan("com.xh.config")
//@Import(UserConfig.class)
//@EnableUser
@Import(User.class)
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
//        Object user = context.getBean("user");
//        System.out.println(user);

        // 由于使用 @Import 注解导入 User.class 获取到的 Bean 名称不叫 user
        // 所以通过类型获取 Bean
        User user = context.getBean(User.class);
        System.out.println("user:" + user);
        // 获取 Spring 容器中所有 UserBean 的名称以及 Bean 对应的值
        Map<String, User> map = context.getBeansOfType(User.class);
        System.out.println("map:" + map);
    }
}

启动项目,成功输出 user 以及 userBeanMap
在这里插入图片描述

2. 导入配置类

使用 @Import(UserConfig.class) 这种方式就是导入配置类,我们可以再创建一个对象并修改配置类的代码,看看一个配置类是否可以加载两个对象
在 enable-other 工程中创建 Role:

package com.xh.domain;

public class Role {
}

在 UserConfig 中添加代码:

    @Bean
    public Role role(){
        return new Role();
    }

修改 enable 工程引导类:

@SpringBootApplication
//@ComponentScan("com.xh.config")
//@EnableUser
//@Import(User.class)
@Import(UserConfig.class)
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
//        Object user = context.getBean("user");
//        System.out.println(user);

        // 由于使用 @Import 注解导入 User.class 获取到的 Bean 名称不叫 user
        // 所以通过类型获取 Bean
        User user = context.getBean(User.class);
        System.out.println("user:" + user);
        Role role = context.getBean(Role.class);
        System.out.println("role:" + role);
        // 获取 Spring 容器中所有 UserBean 的名称以及 Bean 对应的值
//        Map<String, User> map = context.getBeansOfType(User.class);
//        System.out.println("map:" + map);
    }
}

启动项目,成功输出 user 和 role
在这里插入图片描述

3. 导入 ImportSelector 实现类

进入 ImportSelector 接口,我们可以看到 selectImports 方法。
这个方法参数为 AnnotationMetadata,可以用来获取一些注解的值。
返回值为 String 类型的数组,这个方法被复写之后这个方法要返回一些类的全限定名。
在这里插入图片描述
在 enable-other 工程中创建 MyImportSelector 并实现 ImportSelector 接口,复写 selectImports 方法:

放入全限定名加载
package com.xh.config;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 放入 user、role 的全限定名之后,就会自动去加载 user 和对应的 role 了
        // return new String[]{"com.xh.domain.User", "com.xh.domain.Role"};
        
        // 或者可以使用方法获取
        return new String[]{User.class.getName(), Role.class.getName()};
    }
}

修改 enable 工程引导类:

@SpringBootApplication
//@ComponentScan("com.xh.config")
//@EnableUser
//@Import(User.class)
//@Import(UserConfig.class)
@Import(MyImportSelector.class)
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 获取 Bean
//        Object user = context.getBean("user");
//        System.out.println(user);

        // 由于使用 @Import 注解导入 User.class 获取到的 Bean 名称不叫 user
        // 所以通过类型获取 Bean
        User user = context.getBean(User.class);
        System.out.println("user:" + user);
        Role role = context.getBean(Role.class);
        System.out.println("role:" + role);
//        // 获取 Spring 容器中所有 UserBean 的名称以及 Bean 对应的值
//        Map<String, User> map = context.getBeansOfType(User.class);
//        System.out.println("map:" + map);
    }
}

启动项目,成功输出 user 和 role
在这里插入图片描述

读取配置文件动态加载

看到这里的朋友可能会感觉这种方式跟之前的没什么区别,好像还挺麻烦的,其实我们在复写 selectImports 方法时,返回的数组是字符串形式的,我们可以把全限定名添加到配置文件中,那么在项目启动时就可以动态的加载出来。
由于我们在 enable-other 工程中创建的所有类都没有在引导类的同级或者子级目录下,为了方便演示我们要把当前的目录放到 springbootenableother 包下:
在这里插入图片描述
修改 application.properties 配置文件名称为 application.yml,并放入属性值:

name: com.example.springbootenableother.domain.User,com.example.springbootenableother.domain.Role

修改 MyImportSelector,通过读取配置文件实现动态加载:

package com.xh.springbootenableother.config;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class MyImportSelector implements ImportSelector {
    private static Properties properties = new Properties();

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 放入 user、role 的全限定名之后,就会自动去加载 user 和对应的 role 了
        // return new String[]{"com.xh.domain.User", "com.xh.domain.Role"};

        // 或者可以使用方法获取
//        return new String[]{User.class.getName(), Role.class.getName()};

        // 读取配置文件
        InputStream resourceAsStream = Object.class.getResourceAsStream("/application.yml");
        try {
            // 加载配置文件
            properties.load(resourceAsStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 根据 key 获取并拆分返回
        return properties.getProperty("name").split(",");
    }
}

启动项目,成功输出:
在这里插入图片描述

4. 导入 ImportBeanDefinitionRegistrar 实现类

在 enable-other 工程中创建 MyImportBeanDefinitionRegistrar 类,实现 ImportBeanDefinitionRegistrar 接口并重写 registerBeanDefinitions 方法,这种方式是使用注入的形式。

package com.example.springbootenableother.config;

import com.example.springbootenableother.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;

/**
 * @author XH
 * @create 2021/12/13
 * @since 1.0.0
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //在 IOC 容器中注册 Bean,Bean 名称为 user,类型为 User.class
        // 获取 beanDefinition
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        // 注入 user
        registry.registerBeanDefinition("user", beanDefinition);
    }
}

修改 enable 工程中的引导类,可以根据类型或者 Bean 名称获取:

@SpringBootApplication
//@EnableUser
//@Import(MyImportSelector.class)
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringbootEnableApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args);

        // 根据类型获取
        Object user = context.getBean(User.class);
        System.out.println(user);
        // 根据 Bean 名称获取
        Object user1 = context.getBean("user");
        System.out.println(user1);
//        Object role = context.getBean(Role.class);
//        System.out.println(role);
    }
}

启动项目,两种获取方式都可以输出:
在这里插入图片描述

@EnableAutoConfiguration 注解

我们可以看看 Spring 是如何使用 @Import 注解的:
进入引导类上的 @SpringBootApplication 注解可以看到其注解依赖了 @EnableAutoConfiguration 注解。
在这里插入图片描述
进入 @EnableAutoConfiguration 注解可以看到它使用了 @Import 注解导入了 AutoConfigurationImportSelector 这个自动配置的 Selector。
在这里插入图片描述
进入 AutoConfigurationImportSelector 之后我们可以找到 selectImports 方法,根据上文所述,这个方法返回了一个 String 类型的数组,数组中定义了很多需要被加载的类:
在这里插入图片描述
进入加载方法可以看到有一个 getCandidateConfigurations 方法会返回一个名为 configurations 的 List 集合,在下面的代码中根据条件筛选了这个集合并放入创建的 AutoConfigurationEntry 中返回:
在这里插入图片描述
进入 getCandidateConfigurations 可以看到他通过 SpringFactoriesLoader 加载了一些配置信息并返回了一个名为 configurations 的 List 集合,下面的断言表示如果这个集合为空的,那么就会出现异常,大概意思为:“不能自动配置一个 claesses,在 META-INF 目录下的 spring.factories 文件下”,如果没有定义 spring.factories 这个文件,那么他就加载不到,加载不到就会出现断言:
在这里插入图片描述
接下来我们在 External Libraries 中找到 org.springframework.boot:spring-boot-autoconfigure:2.6.1 下的
spring-boot-autoconfigure-2.6.1.jar -> META-INF -> spring.factories 并进入:
在这里插入图片描述
可以看到这个配置文件中有一个 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的配置,其中配置了许多的 Configuration,那么这些 Configuration 将来都会被加载。
当然这些 Configuration 能否加载出来还得看他们的条件是否满足,比如我们可以找到前几个章节讲到的 Redis,Redis 在当前配置文件中有一个 RedisAutoConfiguration:
在这里插入图片描述
进入 RedisAutoConfiguration 我们可以看到其中有一个 @ConditionalOnClass 条件注解,当这个注解里的条件被满足时,这个类中的 Bean 才会被创建。
之前的章节中讲过这个注解,有不了解的朋友可以传送过去看看:SpringBoot 自动配置之 Condition
在这里插入图片描述
当然不止这一个会有条件注解,比如我们再随便挑一个进去看看:
在 KafkaAutoConfiguration 中同样定义了条件注解,当环境中存在 KafkaProperties 时,这个类中的 Bean 才会被加载。
在这里插入图片描述

小结

  • @EnableAutoConfiguration 注解内部使用 @Import(AutoconfigurationImportSelector.class) 来加载配置类。
  • 配置文件位置:META-INF/spring.factories,该配置文件中定义了大量的配置类,当 SpringBoot 应用启动时,会自动加载这些配置类,初始化 Bean。
  • 并不是所有的 Bean 都会被初始化,在配置类中使用 Condition 来加载满足条件的 Bean。
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Spring Boot中,可以使用@Enable注解开启自动配置。要添加自定义的自动配置,可以创建一个@Configuration类,并在该类上添加@Enable注解。例如,假设我们要添加一个名为MyAutoConfiguration的自动配置类: ```java @Configuration @EnableMyAutoConfiguration // 自定义注解 public class MyAutoConfiguration { // ... } ``` 在上面的示例中,我们创建了一个名为MyAutoConfiguration的@Configuration类,并在该类上添加了一个自定义的@EnableMyAutoConfiguration注解。 接下来,我们需要定义@EnableMyAutoConfiguration注解,以便Spring Boot能够识别它并启用我们的自动配置。要定义@EnableMyAutoConfiguration注解,可以使用元注解@EnableAutoConfiguration进行注解。例如: ```java import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @EnableAutoConfiguration public @interface EnableMyAutoConfiguration { } ``` 在上面的示例中,我们定义了一个名为@EnableMyAutoConfiguration的注解,并使用@EnableAutoConfiguration元注解进行注解。这样,Spring Boot就可以识别@EnableMyAutoConfiguration注解,并在启动时启用我们的自动配置。 最后,我们需要将MyAutoConfiguration类打包为一个可执行的Jar包,并将其添加到classpath中。这样,当应用程序启动时,Spring Boot就会自动查找并启用我们的自动配置。 总结一下,要在Spring Boot中添加自定义的自动配置,需要完成以下步骤: 1. 创建一个@Configuration类,其中包含自定义的自动配置代码。 2. 在该类上添加一个自定义的@Enable注解。 3. 定义@Enable注解,以便Spring Boot能够识别它并启用我们的自动配置。 4. 将MyAutoConfiguration类打包为一个可执行的Jar包,并将其添加到classpath中。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值