Spring高级之注解@Import注解、ImportSelector、ImportBeanDefinitionRegistrar详解(超详细)

本文深入探讨Spring框架中@Import注解的使用方法和原理,包括其在配置类之间的引用,以及通过ImportSelector和ImportBeanDefinitionRegistrar进行组件批量注册的技术细节。

定义/作用

@Import注解只能作用在类上,一种使用场景是在spring注解驱动开发环境下与配置类配合使用的,其作用是引用其他配置类。使得我们可以和早起的基于XML配置文件开发那样。使用不同的配置类配置不同的内容,比如Mysql数据源配置用一个配置类。Redis数据源配置用一个配置类等。然后使用在注解在一个主配置类中引入这些从配置类,使得配置更加清晰。被引入的类可以不使用@Configuration、@Component注解。

另一种使用 场景是该注解也是一种注册bean的方案。可以在配置类中使用Import注册组件。可以配合ImportSelector、ImportBeanDefinitionRegistrar按一定规则进行组件的批量注册。

源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * 要引入的配置类,也可以引入ImportSelector、ImportBeanDefinitionRegistrar过滤器和注册器
	 * 按照一定的规则进行组件的引入。
	 */
	Class<?>[] value();

}

使用方式:

不成功的情况:

/**
 * @author YeHaocong
 * @decription 主配置类
 */

@Configuration
public class SpringConfig {

}

/**
 * @author YeHaocong
 * @decription Mysql数据源配置类
 */
@Configuration
public class MysqlConfig {

    @Bean
    public DruidDataSource dataSource() throws IOException {
    	//创建druid数据源
        DruidDataSource dataSource = new DruidDataSource();
        //加载配置文件,作为数据源的初始化属性
        Properties properties = PropertiesLoaderUtils.loadAllProperties("daoconfig/datasource-config.properties");
        dataSource.setConnectProperties(properties);
        //返回dataSource,spring会把他注册到IOC容器中。
        return dataSource;
    }

    //.....
}


//测试类
public class ImportDemoTest {
	//引入主配置类创建容器
    private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

    @Test
    public void testImportDemo(){
        DataSource dataSource = (DataSource) context.getBean("dataSource");
        System.out.println(dataSource);
    }
}

配置文件:
在这里插入图片描述
执行结果:
在这里插入图片描述
分析:因为容器是引入主配置类创建,而没有引入mysql数据源配置类,所以不会扫描创建数据源。

使用Import注解解决:

/**
 * @author YeHaocong
 * @decription 主配置文件
 */

@Configuration
//使用import注解,把其他从配置类引入
@Import({MysqlConfig.class})
public class SpringConfig {

}
/**
 * @author YeHaocong
 * @decription Mysql数据源配置文件
 */
//从配置类可以不使用@Configuration和Component等注解。
public class MysqlConfig {

    @Bean
    public DruidDataSource dataSource() throws IOException {
        DruidDataSource dataSource = new DruidDataSource();
        //加载配置文件,作为数据源的初始化属性
        Properties properties = PropertiesLoaderUtils.loadAllProperties("daoconfig/datasource-config.properties");
        dataSource.setConnectProperties(properties);
        //返回dataSource,spring会把他注册到IOC容器中。
        return dataSource;
    }

    //.....
}

执行结果:
在这里插入图片描述
可见数据源配置类被成功引入,数据源成功创建。

被引入的类会被注册到spring的IOC容器中,并且组件id为类的全限定名称,比如上面的:


public class ImportDemoTest {

    private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

    @Test
    public void testImportDemo(){
        //获取MysqlConfig配置组件
        MysqlConfig mysqlConfig = context.getBean(MysqlConfig.class);
        System.out.println(mysqlConfig);

        //获取注解中所有的组件名称
        String[] beanNames = context.getBeanDefinitionNames();
        for (String beanName:beanNames)
            System.out.println(beanName);
    }
}

结果:
在这里插入图片描述

ImportSelector和ImportBeanDefinitionRegistrar

我们注册bean的方式有很多种。
比如:

  1. 我们自己写的类,可以使用@Component及其衍生类进行注册。
  2. 到导入第三方库时,可以使用@Bean注解和@Import注解进行注册。
    但是,当要注册的类很多时,每个类上加注解,写Bean方法注册,用Import方法导入大量的Bean时,会显得很繁琐,此时可以使用自定义ImportSelector和ImportBeanDefinitionRegistrar来实现组件的批量注册。spring boot有很多EnableXXX的注解,绝大多数多借助了ImportSelector和ImportBeanDefinitionRegistrar。

共同点:

  • 他们都用于动态注册bean对象到容器中,并且支持大批量的bean导入。

区别:

  • ImportSelector是一个接口,我们在使用时需要提供自己的实现类,实现类中重写的方法返回要注册的bean的全限定名数组。然后ConfigurationClassParser类中的precessImports方法注册bean对象。
  • ImportBeanDefinitionRegistrar也是一个接口,需要我们自己提供实现类,在实现类中手动注册bean到容器中。

注意事项:实现了ImportSelector和ImportBeanDefinitionRegistrar的类不会被解析成一个bean添加到容器中。

ImportSelector

demo:
包结构:
在这里插入图片描述
代码:

/**
 * @author YeHaocong
 * @decription 自定义的ImportSelector,导入选择器。
 * 1. 通过AspectJ表达式进行类型筛选。
 * 2. 当使用该选择器的配置类没有使用@ComponentScan注解指定扫描包时,会扫描该配置类所在包及其子包。
 */

public class CustomImportSelector implements ImportSelector {

    //AspectJ表达式
    private String expression;

    public CustomImportSelector() throws IOException {
        try {
            //载入配置文件,创建一个Properties对象
            Properties props = PropertiesLoaderUtils.loadAllProperties("import/custom-import-selector.properties");
            //获取配置文件配置的键为 expression的值,并赋值给expression变量
            expression = props.getProperty("expression");
            if (expression == null || expression.isEmpty()){
                throw new RuntimeException("配置文件import/custom-import-selector.properties 的expression 不存在");
            }
        }
        catch (RuntimeException e){
            throw e;
        }
    }

    /**
     *
     * @param importingClassMetadata 参数是被Import注解作用的配置类的注解元信息
     * @return 返回的是要注册的组件的类的全限定名数组。
     */
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        //定义要扫描的基础包
        String[] basePackages = null;

        //获取ComponentScan注解的全限定名称。
        String ComponentScanName = ComponentScan.class.getName();
        //判断被Import注解作用的类上是否有@ComponentScan注解
        if (importingClassMetadata.hasAnnotation(ComponentScanName)){
            //有@ComponentScan注解,获取该注解上的属性配置,封装成Map对象。
            Map<String,Object> attributes = importingClassMetadata.getAnnotationAttributes(ComponentScanName);
            //获取@ComponentScan注解的value属性或者basePackages属性,因为他们是互为别名,所以获取其中一个即可。
            basePackages = (String[]) attributes.get("basePackages");
        }

        //判断是否有ComponentScan注解或者ComponentScan注解是否有指定扫描包。
        //当basePackages为null时,表示没有ComponentScan注解。
        //当basePackages.length等于0时,表示有basePackages注解,但是没有指定扫描的包。
        if (basePackages == null || basePackages.length == 0){
            //如果@Import注解作用的配置类上没有ComponentScan注解或者有ComponentScan注解但是没有指定扫描包的情况下。
            //我们就扫描该配置类所在包及其子包。

            String basePackage = null;

            //获取被Import注解作用的配置类所在的包。
            try {
                basePackage = Class.forName(importingClassMetadata.getClass().getName()).getPackage().getName();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            //把包名设置到basePackages中。
            basePackages = new String[]{basePackage};
        }

        //创建类路径扫描器,参数的含义是不使用默认的过滤规则,与@ComponentScan注解的 useDefaultFilters属性一样。
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        //创建类型过滤器,此处使用AspectJ类型过滤器。传入参数是AspectJ表达式和类加载器对象
        TypeFilter typeFilter = new AspectJTypeFilter(expression,CustomImportSelector.class.getClassLoader());

        //类型过滤器添加到扫描器中。添加的是包含扫描器。
        scanner.addIncludeFilter(typeFilter);



        //定义要扫描类的全限定类名的集合
        Set<String> classes = new HashSet<>();

        //遍历基础扫描类数组,得到要扫描的类的全限定名,并添加到集合中
        for (String basePackage: basePackages){
            //扫描基础包,获取扫描到的BeanDefinition集合
            Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
            //遍历。获取全限定名添加到集合中。
            for (BeanDefinition beanDefinition: candidateComponents){
                classes.add(beanDefinition.getBeanClassName());
            }

        }

        //返回集合
        return classes.toArray(new String[classes.size()]);
    }
}


/**
*配置类
*/

@Configuration
@Import({CustomImportSelector.class})
public class SpringConfig {
}

//还有两个业务接口和两个业务接口实现类和一个ConfigUtil,这两个业务实现类和ConfigUtil类都是要注册的组件。这里不再贴出,可以看上面包结构。

public class TestImportSelector {

    private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

    @Test
    public void TestImportSelector(){
        //根据类型获取bean
        try {
            ConfigUtil configUtil = (ConfigUtil) context.getBean(ConfigUtil.class);
            System.out.println(configUtil);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        try {
            RoleService roleService = (RoleService) context.getBean(RoleService.class);
            System.out.println(roleService);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        try {
            UserService userService = (UserService) context.getBean(UserService.class);
            System.out.println(userService);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }

    }
}


配置文件:
在这里插入图片描述
执行结果:
在这里插入图片描述
分析:一个bean都没有注册成功,原因是:
配置类SpringConfig上没有使用@ComponentScan或者使用了但是没有配置扫描包。所以会扫描配置类所在包及其子包,看上面包结果。明显没有扫描到service包。所以两个业务实现类没有被注册到容器中。而ConfigUtil虽然被扫描到了,但是由于不符合AspectJ表达式而没有被添加到选择器中。

接下来我们使用@ComponentScan扫描指定包。

@Configuration
@ComponentScan(basePackages = "importselectdemo")
@Import({CustomImportSelector.class})
public class SpringConfig {
}

/**
*测试类
*/
public class TestImportSelector {

    private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);

    @Test
    public void TestImportSelector(){
        //根据类型获取bean
        try {
            ConfigUtil configUtil = (ConfigUtil) context.getBean(ConfigUtil.class);
            System.out.println(configUtil);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        try {
            RoleService roleService = (RoleService) context.getBean(RoleService.class);
            System.out.println(roleService);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        try {
            UserService userService = (UserService) context.getBean(UserService.class);
            System.out.println(userService);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        try {
            CustomImportSelector selector = (CustomImportSelector) context.getBean(CustomImportSelector.class);
            System.out.println(selector);
        }catch (Exception e){
            System.out.println(e.getMessage());
        }


        String[] beanNames = context.getBeanDefinitionNames();
        for (String beanName:beanNames){
            System.out.println(beanName);
        }
    }
}

执行结果:
在这里插入图片描述

注意:不能导入配置类自身,因为,这样会导致报错。
将表达式设置为:在这里插入图片描述
结果:

org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: A circular @Import has been detected: Illegal attempt by @Configuration class 'SpringConfig' to import class 'SpringConfig' as 'SpringConfig' is already present in the current import stack [SpringConfig->SpringConfig]
Offending resource: importselectdemo.config.SpringConfig

使用上述方法只要符合CustomImportSelector规则,即使不使用@Component等注解也会注册到容器中。

ImportBeanDefinitionRegistrar

这个注册器不会把扫描到的类返回,而是把扫描到的类直接就在这里注册了。

demo(扫描逻辑与上面的CustomImportSelector一样):

/**
 * @author YeHaocong
 * @decription 自定义的ImportBeanDefinitionRegistrar,导入注册器。
 * 1. 通过AspectJ表达式进行类型筛选。
 * 2. 当使用该选择器的配置类没有使用@ComponentScan注解指定扫描包时,会扫描该配置类所在包及其子包。
 * 3. CustomImportDefinitionRegistrar会扫描指定包里,符合AspectJ表达式的组件的类进行注册
 */

public class CustomImportDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    //AspectJ表达式
    private String expression;

    public CustomImportDefinitionRegistrar() throws IOException {
        try {
            //载入配置文件,创建一个Properties对象
            Properties props = PropertiesLoaderUtils.loadAllProperties("import/custom-import-selector.properties");
            //获取配置文件配置的键为 expression的值,并赋值给expression变量
            expression = props.getProperty("expression");
            if (expression == null || expression.isEmpty()){
                throw new RuntimeException("配置文件import/custom-import-selector.properties 的expression 不存在");
            }
        }
        catch (RuntimeException e){
            throw e;
        }
    }

    /**
     * 
     * @param importingClassMetadata  参数是被Import注解作用的配置类的注解元信息
     * @param registry   BeanDefinition注册器,会将扫描到的类直接使用该注册器进行注册
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //定义要扫描的基础包
        String[] basePackages = null;

        //获取ComponentScan注解的全限定名称。
        String ComponentScanName = ComponentScan.class.getName();
        //判断被Import注解作用的类上是否有@ComponentScan注解
        if (importingClassMetadata.hasAnnotation(ComponentScanName)){
            //有@ComponentScan注解,获取该注解上的属性配置,封装成Map对象。
            Map<String,Object> attributes = importingClassMetadata.getAnnotationAttributes(ComponentScanName);
            //获取@ComponentScan注解的value属性或者basePackages属性,因为他们是互为别名,所以获取其中一个即可。
            basePackages = (String[]) attributes.get("basePackages");
        }

        //判断是否有ComponentScan注解或者ComponentScan注解是否有指定扫描包。
        //当basePackages为null时,表示没有ComponentScan注解。
        //当basePackages.length等于0时,表示有basePackages注解,但是没有指定扫描的包。
        if (basePackages == null || basePackages.length == 0){
            //如果@Import注解作用的配置类上没有ComponentScan注解或者有ComponentScan注解但是没有指定扫描包的情况下。
            //我们就扫描该配置类所在包及其子包。

            String basePackage = null;

            //获取被Import注解作用的配置类所在的包。
            try {
                basePackage = Class.forName(importingClassMetadata.getClass().getName()).getPackage().getName();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            //把包名设置到basePackages中。
            basePackages = new String[]{basePackage};
        }

        //创建类路径扫描器ClassPathBeanDefinitionScanner,参数的含义是不使用默认的过滤规则,与@ComponentScan注解的 useDefaultFilters属性一样。
        //registry参数是将扫描到的类使用指定的registry注册器注册
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry,false);
        //创建类型过滤器,此处使用AspectJ类型过滤器。传入参数是AspectJ表达式和类加载器对象
        TypeFilter typeFilter = new AspectJTypeFilter(expression,CustomImportSelector.class.getClassLoader());

        //类型过滤器添加到扫描器中。添加的是包含扫描器。
        scanner.addIncludeFilter(typeFilter);

        //进行扫描
        scanner.scan(basePackages);



    }
}


//配置类:
@Configuration
@ComponentScan(basePackages = "importselectdemo")
//使用CustomImportDefinitionRegistrar
@Import({CustomImportDefinitionRegistrar.class})
public class SpringConfig {
}

结果:
在这里插入图片描述
解析:

  1. ConfigUtil不符合AspectJ表达式规则,所以没有注册。
  2. 业务类注册成功。
  3. 实现ImportBeanDefinitionRegistrar接口的类不会被添加到容器中。
  4. 因为使用的是BeanDefinitionRegistry注册器,所以注册的bean id 默认是类的名字第一个转小写。而不是全限定名称。

使用上述方法只要符合CustomImportDefinitionRegistrar规则,即使不使用@Component等注解也会注册到容器中。

<think>我们被要求详细解释Spring框架中的@Import注解。根据提供的引用内容,我们可以总结如下: 引用[1]和[2]指出,@Import注解用于导入配置类,实现配置的模块化。它可以将一个或多个配置类引入到当前配置类中,类似于Spring XML配置文件中的`<import>`标签。@Import有三种使用方式:普通方式、ImportSelector方式、ImportBeanDefinitionRegistrar方式。 引用[4]展示了第一种方式:直接引入Bean。在配置类上使用@Import(ImportedBean.class),就可以将ImportedBean注册到Spring容器中。 此外,引用[3]提到了在处理@Import注解时,会通过beanDefinition的元数据(metadata)来获取类的注解信息,进而处理@Import注解中的内容。 因此,我们需要详细解释@Import注解的三种用法,并给出相应的示例。 ### 详细解释 #### 1. 普通方式 直接导入一个或多个配置类或普通类(这些类会被注册为Bean)。语法:`@Import({类1.class, 类2.class, ...})` 示例: ```java public class MyService {} @Configuration @Import(MyService.class) public class AppConfig {} ``` 这样,MyService就会被注册为一个Spring Bean。 #### 2. ImportSelector方式 通过实现`ImportSelector`接口,可以编程式地选择要导入的配置类。该接口有一个方法`String[] selectImports(AnnotationMetadata importingClassMetadata)`,返回要导入的类的全限定名的数组。 示例: ```java public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 根据条件返回需要导入的类 return new String[]{"com.example.ServiceA", "com.example.ServiceB"}; } } @Configuration @Import(MyImportSelector.class) public class AppConfig {} ``` #### 3. ImportBeanDefinitionRegistrar方式 通过实现`ImportBeanDefinitionRegistrar`接口,可以编程式地注册Bean定义。该接口有一个方法`void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)`,可以在其中使用BeanDefinitionRegistry来注册Bean。 示例: ```java public class MyBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 创建Bean定义 RootBeanDefinition beanDefinition = new RootBeanDefinition(MyBean.class); // 注册Bean定义 registry.registerBeanDefinition("myBean", beanDefinition); } } @Configuration @Import(MyBeanDefinitionRegistrar.class) public class AppConfig {} ``` ### 总结 @Import注解Spring中用于模块化配置的重要注解,它提供了三种不同的方式来导入配置或Bean: 1. 直接导入类(普通方式) 2. 通过ImportSelector接口动态选择导入的类 3. 通过ImportBeanDefinitionRegistrar接口编程式注册Bean定义 使用@Import注解可以避免使用XML配置来组合配置,使得配置更加灵活和模块化。 ### 相关问题 1. @Import注解和@ComponentScan注解有什么区别? 2. 如何在使用@Import导入的配置类中使用@Autowired注入其他Bean? 3. 在Spring Boot中,@Import注解通常用在哪些场景? </think>### Spring @Import 注解详细解释 `@Import` 是 Spring 框架的核心注解之一,用于**动态注册 Bean 到 IoC 容器**,实现配置的模块化。它有三种使用方式,每种方式对应不同的场景需求: #### 方式一:直接导入类(普通方式) - **作用**:将指定类注册为 Spring Bean - **语法**:`@Import(ClassName.class)` - **示例**: ```java public class DatabaseService {} // 待注册的类 @Configuration @Import(DatabaseService.class) // 直接导入 public class AppConfig {} ``` - **特点**: - 适用于简单场景 - 被导入的类无需额外注解(如 `@Component`) - 通过 `ApplicationContext.getBean(DatabaseService.class)` 可直接获取[^4] #### 方式二:实现 `ImportSelector` 接口 - **作用**:**动态选择**需要注册的 Bean - **核心方法**: ```java public interface ImportSelector { String[] selectImports(AnnotationMetadata importingClassMetadata); } ``` - **示例**: ```java public class DynamicImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata metadata) { // 根据条件动态返回类名 if (isProduction()) { return new String[]{ProdDataSource.class.getName()}; } return new String[]{DevDataSource.class.getName()}; } } @Configuration @Import(DynamicImportSelector.class) public class EnvConfig {} ``` - **特点**: - 实现条件化 Bean 注册 - 可读取导入类的元数据(`AnnotationMetadata`) - Spring Boot 的 `@EnableAutoConfiguration` 底层采用此机制[^2] #### 方式三:实现 `ImportBeanDefinitionRegistrar` 接口 - **作用**:**编程式注册 Bean 定义**(最灵活的方式) - **核心方法**: ```java public interface ImportBeanDefinitionRegistrar { void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry ); } ``` - **示例**: ```java public class CustomBeanRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 创建 Bean 定义 BeanDefinition definition = new RootBeanDefinition(CustomService.class); // 手动注册 Bean registry.registerBeanDefinition("customService", definition); } } @Configuration @Import(CustomBeanRegistrar.class) public class CustomConfig {} ``` - **特点**: - 完全控制 Bean 的注册过程 - 可访问 `BeanDefinitionRegistry` 进行高级操作 - 支持解析被导入类的元数据(如注解信息)[^3] --- ### 核心机制 1. **处理时机**:在容器启动时,Spring 解析 `@Import` 注解: - 检查 `BeanDefinition` 的元数据 (`getMetadata()`)[^3] - 递归处理所有导入的类或接口 2. **与 XML 等效操作**:等同于 XML 配置中的 `<import>` 标签: ```xml <!-- XML 等效配置 --> <import resource="database-config.xml"/> ``` 3. **模块化优势**: - 解耦大型项目配置 - 实现按需加载(如根据 Profile 加载不同配置) - 支持第三方库的无侵入整合(常见于 Spring Boot Starter) > 提示:三种方式可组合使用,例如 `@Import({ClassA.class, SelectorB.class})`
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值