Spring中的@import註解使用詳解

一、簡介

@import註解對應着Spring XML文件中的 <import />,@import註解可以註冊組件,也可以將另一加載類中的Bean加載到當前配置類。

@import註解支持導入的類有以下三種:

  • @Configuration註解修飾的配置類
  • 實現ImportSelector接口的實現類
  • 實現ImportBeanDefinitionRegistrar接口的實現類

在 Spring 4.2後,@import 也支持對常規組件類的引用,類似於 AnnotationConfigApplicationContext.register 方法。

二、使用

1.導入@Configuration註解修飾的配置類

假設有以下兩個配置類,將SpringConfig2加載到SpringConfig中,同時SpringConfig配置類還加載普通類Yellow,代碼配置如下:

@Configuration
@Import({SpringConfig2.class,Yellow.class})
public class SpringConfig {
	@Bean("person")
	public Person getPerson() {		
		return new Person();
	}
}
@Configuration
public class SpringConfig2 {
	@Bean("person2")
	public Person getPerson() {	
		return new Person();
	}
}

創建測試類,加載SpringConfig配置類,查看Ioc容器中創建所有的Bean實例:

public class IoCTest {
	@Test
	public void test01() {
		//獲取Spring的IOC容器
		ApplicationContext applicationContext=new AnnotationConfigApplicationContext(SpringConfig.class);
		//從容器中獲取bean
	    String[] names= applicationContext.getBeanDefinitionNames();	
	       for(String i:names) {
	    	   System.out.println(i);
	       }
	}
}

可以看到 SpringConfig2 定義的Bean和普通的組件類Yellow也成功配置到容器中。其中不帶任何註解的Yellow類的默認Bean的id爲其全類名。結果如下:
在這裏插入圖片描述
當有多個配置類時,使用這種方法簡化了容器的實例化,因爲只需要處理一個類,而不需要開發人員在構建過程中記住大量的@configuration 類。

2.實現ImportSelector接口的實現類

創建一個類實現ImportSelector接口,如下:

//自定義邏輯返回需要導入的組件
public class MyImportSelector implements ImportSelector {
	/*
	 * 該方法返回字符串數組,要求數組元素爲全類名,用於執行需要註冊爲Bean的Class名稱
	 * 參數AnnotationMetadata對象可以獲取使用了@import註解的類的各種信息,
	 * 包括其Class名稱,實現的接口名稱、父類名稱、添加的其它註解等信息。
	 */
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		return new String[]{"com.learn.bean.Red","com.learn.bean.Blue"};
	}
}

在配置類中的@import註解添加這個類,就完成了Bean的配置

@Configuration
@Import({MyImportSelector.class})
public class SpringConfig {
	@Bean("person")
	public Person getPerson() {
			return new Person();
	}
}

這樣在加載SpringConfig配置類時就會將com.learn.bean.Red和com.learn.bean.Blue類註冊爲Bean,雖然這個也能夠直接在@import註解中直接導入相關的類,但是ImportSelector實現類可以實現根據自定義的邏輯來定義Bean。

例如,要實現當配置類上的@ComponentScan沒有指定要掃描的package或沒有使用@ComponentScan註解的時候,就默認把當前配置類所在的包當作@ComponentScan註解的basePackages屬性,對這個包的類進行掃描。

在importSelector實現類就可以這樣定義,如下:

//自定義邏輯返回需要導入的組件
public class MyImportSelector implements ImportSelector {
	/*
	 * 該方法返回字符串數組,要求爲全類名,用於執行需要註冊爲Bean的Class名稱
	 * 參數AnnotationMetadata對象可以獲取使用了@import註解的類的各種信息,包括其Class名稱,實現的接口名稱、父類名稱、添加的其它註解等信息。
	 */
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		String[] basePackages=null;
		if (importingClassMetadata.hasAnnotation(ComponentScan.class.getName())) {
			//獲取ComponentScanner註解的所有屬性
			Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getName());
			//獲取basePackages屬性值
			basePackages = (String[]) annotationAttributes.get("basePackages");
		}
		if (basePackages==null||basePackages.length==0) {
			String basePackage=null;
			//沒有定義basePackages就將當前包作爲要掃描的包
			try {
				basePackage=Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
				System.out.println("------->"+basePackage);
			} catch (ClassNotFoundException e) {
				// TODO: handle exception
				e.printStackTrace();
			}
			
			basePackages=new String[]{basePackage};		
		}		

	    // true:默認TypeFilter生效,當需要自定義特定的類時使用false,關閉TypeFilter。
		//這種模式會查詢出spring自帶的註解
		ClassPathScanningCandidateComponentProvider scanner=new ClassPathScanningCandidateComponentProvider(true);
        //自定義掃描規則,這裏需要排除掉@Configuration註解,
		//因爲@Configuration註解在容器創建時就先裝配到容器中了
		scanner.addExcludeFilter(new AnnotationTypeFilter(Configuration.class));
		//掃描實現指定接口的類,接口不會被掃描
		//scaner.addIncludeFilter(new AssignableTypeFilter(targetType));
		Set<String> classes = new HashSet<>();
		for (String basePackage : basePackages) {
			Set<BeanDefinition> findCandidateComponents = scanner.findCandidateComponents(basePackage);
			for (BeanDefinition beanDefinition : findCandidateComponents) {				
				classes.add(beanDefinition.getBeanClassName());				
			}
		}
		//返回掃描到的類
		return classes.toArray(new String[classes.size()]);
	}
}

通過ClassPathScanningCandidateComponentProvider(true)的構造器,就可以掃描出任何出任何我們添加了@Component註解的類,那麼它是怎麼做到的呢? 我們來看一下這個構造方法傳入的是true,那麼就會調用到這個方法:

	public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) {
		if (useDefaultFilters) {
			registerDefaultFilters();
		}
		setEnvironment(environment);
		setResourceLoader(null);
	}
	
	@SuppressWarnings("unchecked")
	protected void registerDefaultFilters() {
		this.includeFilters.add(new AnnotationTypeFilter(Component.class));
//省略

可以看到,當我們在構造方法裏面傳入了true參數之後,對象的includeFilters屬性就添加了一個AnnotationTypeFilter對象,並且此對象的參數是Component註解類。
另外ClassPathScanningCandidateComponentProvider有如下兩個屬性:

private final List<TypeFilter>includeFilters=newLinkedList<TypeFilter>();

private final List<TypeFilter>excludeFilters=newLinkedList<TypeFilter>();

一個是包含的過濾器,一個是排除的過濾器,所以ClassPathScanningCandidateComponentProvider提供了類似@ComponentScan註解的功能。

類似於上面的例子,我們可以自己寫一個繼承ClassPathScanningCandidateComponentProvider的類,然後聲明一個自己的annotation,然後掃描特定的包,我們也能掃描出相應的類名,然後使用Java反射機制,就可以構造出自己想要的對象。同時這裏的TypeFilter我們也可以進行相應的定製,不僅僅侷限於annotation,還可以是過濾實現了某個接口的類。

3.實現ImportBeanDefinitionRegistrar接口的實現類

ImportBeanDefinitionRegistrar的用法和作用跟ImportSelector類似。唯一的不同點是ImportBeanDefinitionRegistrar的接口方法void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)的返回類型是void,且多了一個BeanDefinitionRegistry類型的參數,它允許我們直接通過BeanDefinitionRegistry對象註冊bean。

下面使用ImportBeanDefinitionRegistrar接口實現類來實現沒有定義掃描的包時就掃描配置類所在包的類的功能。代碼如下:

public class MyImportBeanDefinitionRegistrar implements  ImportBeanDefinitionRegistrar {
	/**
	 *  AnnotationMetadata :包含了使用了@import註解的類的所有信息,
	 *  包括其Class名稱,實現的接口名稱、父類名稱、添加的其它註解等信息。
	 *  BeanDefinitionRegistry:已經完成註冊的組件信息
	 */
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		String[] basePackages=null;
		if (importingClassMetadata.hasAnnotation(ComponentScan.class.getName())) {
			//獲取ComponentScanner註解的所有屬性
			Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getName());
			//獲取basePackages屬性值
			basePackages = (String[]) annotationAttributes.get("basePackages");
		}
		if (basePackages==null||basePackages.length==0) {
			String basePackage=null;
			//沒有定義basePackages就將當前包作爲要掃描的包
			try {
				basePackage=Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
				System.out.println("------->"+basePackage);
			} catch (ClassNotFoundException e) {
				// TODO: handle exception
				e.printStackTrace();
			}
			
			basePackages=new String[]{basePackage};		
		}		

	    // 默認TypeFilter生效,這種模式會查詢出spring自帶的註解
		ClassPathBeanDefinitionScanner scanner=new ClassPathBeanDefinitionScanner(registry);
		//當需要自定義特定的類時使用這個構造器創建,參數選擇false,關閉默認的TypeFilter。
		//ClassPathBeanDefinitionScanner scanner2 = new ClassPathBeanDefinitionScanner(registry, false);

		//掃描實現指定接口的類,接口不會被掃描
		//scaner.addIncludeFilter(new AssignableTypeFilter(targetType));
	     scanner.scan(basePackages);
	     
		// 實現當容器中存在Bean才註冊另一個Bean
		boolean b=registry.containsBeanDefinition("person");
		if (b) {
			//定義Bean的類型
			RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Yellow.class);
			//定義Bean的id
			registry.registerBeanDefinition("yellow", rootBeanDefinition);
		}		
	}
}

因爲registerBeanDefinitions方法不需要返回值,在實現類中可以使用ClassPathBeanDefinitionScanner類進行掃描並自動註冊,它是ClassPathScanningCandidateComponentProvider的子類,提供了scanner.scan(basePackages)方法掃描指定的basePackage下滿足條件的Class並註冊它們爲bean。

參考:
Spring(32)——ImportSelector介紹
Spring(33)——ImportBeanDefinitionRegistrar介紹

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