@Import、ImportSelector注解使用及源码分析

6,046 阅读5分钟

一、@Import

在学习@Import这个注解时,小编在想一个问题,这个注解的作用是导入一个配置Configuration类,那到底什么地方会用到它呢?想到我们工程中也不会使用这个注解去导入配置呀,我们都是新建一个类xxxxxxConfiguration.java,然后直接在类里边把所有的Bean组件啥的都给声明了,下面的代码我们感觉似曾相识,哈哈。

/**
 * xx配置类,里边会有n个bean
 * @Author jiawei huang
 * @Since 2019年8月26日
 * @Version 1.0
 */
@Configuration
public class CustomConfig {
    @Bean
    public Marker zuulProxyMarkerBean() {
    	return new Marker();
    }
    ......
}

但你有没有想过一个问题,当配置类CustomConfig不在@SpringBootApplication所在包及其子包下时,它还能被装配进去吗?答案是不能。因为,它不在springboot默认扫描范围内。详情可查看SpringBoot封装我们自己的Starter

我讲的到底有没有道理呢?让我们来做个实验。UserConfig用于配置User对象,它位于com.example包下,DemoApplication.java位于com.example.demo包下,此时SpringBoot是没法扫描到UserConfig并注入User对象的。

UserConfig.java

/**
 * @Author jiawei huang
 * @Since 2019年8月26日
 * @Version 1.0
 */
@Configuration
public class UserConfig {
    @Bean
    public User getUser() {
    	return new User();
    }
}

使用下面代码注入会报错:

@Autowired
private User user;
The injection point has the following annotations:
	- @org.springframework.beans.factory.annotation.Autowired(required=true)

怎么办呢?解决办法有二种:

  • 1、使用@ComponentScan("com.**")注解一句话搞定
  • 2、使用@Import注解引入

方法一简单粗暴,看似没啥毛病,但这是建立在你知道bean对象的大概包路径的基础上的,第三方的jar包中的bean可并不是都是以com开头命名的,这就尴尬了。 在上面的路径结构基础上,我们在DemoApplication.java中加入@Import(UserConfig.class)这个注解即可解决问题。

另外,@Import相当于Spring xml配置文件中的<import />标签。

二、ImportSelector

@Import注释是让我们导入一组指定的配置类--@Configuration修饰的类,类名一旦指定,将全部被解析。相反,ImportSelector将允许我们根据条件动态选择想导入的配置类,换句话说,它具有动态性。ImportSelector使用时,我们要创建一个类实现ImportSelector接口,并重写其中的String[] selectImports(AnnotationMetadata importingClassMetadata);方法。

假设我们想实现这样一个功能,我们创建一个CustomImportSelector类,当使用CustomImportSelector的元素是类时,我们返回UserConfig配置类,当使用CustomImportSelector的元素是接口时,我们返回StudentConfig配置类。

注意目录层次,要保证UserConfigStudentConfigDemoApplication的外层,否则,这两个配置类就会被spring默认解析到了。

/**
 * 
 * @Author jiawei huang
 * @Since 2019年8月26日
 * @Version 1.0
 */
@Configuration
public class UserConfig {
	@Bean
	public User getUser() {
		return new User();
	}
}
/**
 * 
 * @Author jiawei huang
 * @Since 2019年8月26日
 * @Version 1.0
 */
@Configuration
public class StudentConfig {

	@Bean
	public Student getStudent() {
		return new Student();
	}

}
@SpringBootApplication
// 1、很明显,这里CustomImportSelector修饰的是一个类,我们将会返回UserConfig
@Import(CustomImportSelector.class)
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}
/**
 * 
 * @Author jiawei huang
 * @Since 2019年8月19日
 * @Version 1.0
 */
@RestController
public class MyController {

	@Autowired(required = false)
	private Student student;

	@Autowired(required = false)
	private User user;

	@RequestMapping("/getStudent")
	private String getStudent() {
		return "student=[" + student + "],user=[" + user + "]";
	}

}
/**
 * 
 * @Author jiawei huang
 * @Since 2019年8月26日
 * @Version 1.0
 */
public class CustomImportSelector implements ImportSelector {

	/**
	 * importingClassMetadata:被修饰的类注解信息
	 */
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {

		// 注意,自定义注解这里是拿不到的
		System.out.println(importingClassMetadata.getAnnotationTypes());

		// 如果被CustomImportSelector导入的组件是类,那么我们就实例化UserConfig
		if (!importingClassMetadata.isInterface()) {
			return new String[] { "com.example.UserConfig" };
		}

		// 此处不要返回null
		return new String[] { "com.example.StudentConfig" };
	}
}

打开浏览器,调用接口,得到如下返回,证明Student没有被注入成为bean,而User成功被注入

三、讲讲原理

注解在Spring启动过程中在哪里被解析? Spring源码版本:5.1.6.RELEASE

小编粗略debug了下源码,这2个注解的解析过程统一在ConfigurationClassParser$DeferredImportSelectorGroupingHandler类中的processImports()方法实现的,该方法大致源码如下:

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
    	Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
    
    if (importCandidates.isEmpty()) {
    	return;
    }
    
    if (checkForCircularImports && isChainedImportOnStack(configClass)) {
    	this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
    }
    else {
    	this.importStack.push(configClass);
        try {
        for (SourceClass candidate : importCandidates) {
            // 1、如果该配置类被ImportSelector修饰,则当成ImportSelector进行处理
        	if (candidate.isAssignable(ImportSelector.class)) {
        		// Candidate class is an ImportSelector -> delegate to it to determine imports
        		Class<?> candidateClass = candidate.loadClass();
        		ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
        		ParserStrategyUtils.invokeAwareMethods(
        				selector, this.environment, this.resourceLoader, this.registry);
        		if (selector instanceof DeferredImportSelector) {
        			this.deferredImportSelectorHandler.handle(
        					configClass, (DeferredImportSelector) selector);
        		}
        		else {
        			String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
        			Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
        			processImports(configClass, currentSourceClass, importSourceClasses, false);
        		}
        	}
        	// 2、如果该配置类被ImportBeanDefinitionRegistrar修饰,则当成ImportBeanDefinitionRegistrar进行处理
        	else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
        		// Candidate class is an ImportBeanDefinitionRegistrar ->
        		// delegate to it to register additional bean definitions
        		Class<?> candidateClass = candidate.loadClass();
        		ImportBeanDefinitionRegistrar registrar =
        				BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
        		ParserStrategyUtils.invokeAwareMethods(
        				registrar, this.environment, this.resourceLoader, this.registry);
        		configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
        	}
        	// 3、如果该配置类被Import修饰,则当成Import进行处理
        	else {
        		// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
        		// process it as an @Configuration class
        		this.importStack.registerImport(
        				currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
        		processConfigurationClass(candidate.asConfigClass(configClass));
        	}
        }
        }
        catch (BeanDefinitionStoreException ex) {
        throw ex;
        }
        catch (Throwable ex) {
        throw new BeanDefinitionStoreException(
        		"Failed to process import candidates for configuration class [" +
        		configClass.getMetadata().getClassName() + "]", ex);
        }
        finally {
        this.importStack.pop();
        }
    }
}

从Spring启动开始,到执行注解解析,大致调用链路如下:

SpringApplication-refreshContext()->AbstractApplicationContext-refresh()-postProcessBeanFactory()->PostProcessorRegistrationDelegate-invokeBeanDefinitionRegistryPostProcessors()->ConfigurationClassPostProcessor-processConfigBeanDefinitions()->ConfigurationClassParser-parse()->ConfigurationClassParser-processImports()

ConfigurationClassParserSpring提供的用于解析@Configuration的配置类,通过它将会得到一个ConfigurationClass对象列表。

四、总结

其实一般在项目上,我们实在是用不到上面的注解。有时候知识我们学会了,但是我们总想不出一种应用场景来将技术给用上,好烦。其实并不是这样的,了解技术的来龙去脉,久而久之会给我们带来很多能力,比如编写更加优秀的代码,更容易看懂框架源码,框架上手快,bug解决速度快,牛逼吹起来会更有逼格。

但是,脱离需求,技术可能意义不是很大,接到一个需求,我们可以动动脑,看下这个需求能不能用上,就好比下面这张购物车实现图:

像这些商品数量的操作,我们完全可以使用redis的相关操作来实现,你却非要给我建一张表来存储,当然不是不可以,只是缓存更简单,更高效罢了。以用户id为key,商品id作为field,使用redis哈希这种数据结构即可解决。

小编觉得先不急着实现需求,可以先多动动脑筋,看看有什么技术点可以用到,再动手写代码。