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介绍

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