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等註解也會註冊到容器中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章