1. 程式人生 > 程式設計 >@Import、ImportSelector註解使用及原始碼分析

@Import、ImportSelector註解使用及原始碼分析

一、@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.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雜湊這種資料結構即可解決。

小編覺得先不急著實現需求,可以先多動動腦筋,看看有什麼技術點可以用到,再動手寫程式碼。