@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
配置類。
UserConfig
和StudentConfig
在DemoApplication
的外層,否則,這兩個配置類就會被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()
ConfigurationClassParser
是Spring
提供的用於解析@Configuration
的配置類,通過它將會得到一個ConfigurationClass
物件列表。
四、總結
其實一般在專案上,我們實在是用不到上面的註解。有時候知識我們學會了,但是我們總想不出一種應用場景來將技術給用上,好煩。其實並不是這樣的,瞭解技術的來龍去脈,久而久之會給我們帶來很多能力,比如編寫更加優秀的程式碼,更容易看懂框架原始碼,框架上手快,bug解決速度快,牛逼吹起來會更有逼格。
但是,脫離需求,技術可能意義不是很大,接到一個需求,我們可以動動腦,看下這個需求能不能用上,就好比下面這張購物車實現圖:
像這些商品數量的操作,我們完全可以使用redis的相關操作來實現,你卻非要給我建一張表來儲存,當然不是不可以,只是快取更簡單,更高效罷了。以使用者id為key,商品id作為field,使用redis雜湊這種資料結構即可解決。
小編覺得先不急著實現需求,可以先多動動腦筋,看看有什麼技術點可以用到,再動手寫程式碼。