1. 前言
很多時候我們的Spring項目使用多子產品,或者我們需要将自己特定的類庫打成依賴。預設情況下Spring Boot應用隻會掃描main方法所在的包路徑下的Bean和通過
spring.factories
進行注冊發現自動裝配到Spring IoC中去。像下面這個Maven項目中,如果Spring Boot的Main類在
cn.felord.yaml
包下的話
cn.felord.common
包的Spring Bean是無法被掃描注冊到Spring IoC容器中的。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SN0kzN4YDZwkzYzcTN5IjMzYzX3ATMwETM1EzLclDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
Maven多目錄項目
今天我們将借助于
@Import
注解和相關的一些接口來實作特定路徑下的Spring Bean的導入。
2. @Import
@Import
注解主要提供配置類導入的功能。我們可以從Spring Boot的很多
@EnableXX
注解中發現它的影子,例如開啟緩存注解
@EnableCaching
、開啟異步注解
@EnableAsync
等等。它提供了半自動的功能,讓我們可以即使引入了對應的依賴時也可以手動來控制一些配置的生效。
package cn.felord.common;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author felord.cn
*/
public class CommonConfiguration {
@Bean
public FooService fooService(){
return () -> "@Import";
}
}
以上是我們在
cn.felord.common
下實作的一個配置,目的是将
FooService
的實作注冊為
Spring Bean
。可能很多同學會想到使用
@ComponentScan("cn.felord.common")
來實作,這當然是可以實作的。問題在于這個聲明講所有在
cn.felord.common
包下的Spring Bean都注冊了,控制的粒度比較粗。如果我們想控制的粒度細一些,指定哪些被導入哪些不被導入,使用
@Import
就再好不過了。
@Import
可以将
@Configuration
标記的類、
ImportSelector
的實作類以及
ImportBeanDefinitionRegistrar
的實作類導入。在Spring 4.2版本以後,普通的類(如上面代碼中的
CommonConfiguration
)也可以被導入,将其注冊為Spring Bean。
@Import(CommonConfiguration.class)
我們可以很容易地利用
@Import
注解開發出一些
@EnableXX
注解來控制一些功能是否生效。
/**
* @author felord.cn
* @since 10:35
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CommonConfiguration.class)
public @interface EnableCommon {
}
我們也可以通過
ImportSelector
接口來實作更加強大的功能
3. ImportSelector
ImportSelector
接口是按照給定的标準,通常是根據一到多個注解參數來決定那個配置類應該被導入。也就是說我們可以在上面的
@EnableCommon
注解中添加注解參數來實作更加靈活的導入。
public interface ImportSelector {
/**
* 基于導入的配置類的注解元資訊來檢出并決定哪些類應該被導入。
* 傳回被導入的類的全限定名數組,如果沒有則傳回一個空數組。
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
/**
* 傳回一個謂詞接口,該接口制定了一個對類全限定名的排除規則來過濾一些候選的導入類,預設不排除過濾。
*
* @since 5.2.4
*/
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
第一個方法
selectImports
我們大緻上可以了解為通過
importingClassMetadata
提供的資訊來決定哪些類導入。如果存在第二個方法
getExclusionFilter
的實作。會對
selectImports
方法的傳回值進行過濾,最終輸出哪些配置類可以導入Spring IoC。
但是
importingClassMetadata
從哪裡來可能是我們最想知道的,我們來一探究竟。先寫一個配置類:
/**
* @author felord.cn
* @since 10:27
**/
public class BarConfiguration {
@Bean
public Function<String, Integer> stringLength() {
return String::length;
}
}
實作
ImportSelector
:
/**
* @author felord.cn
* @since 10:19
**/
public class CommonImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
System.out.println("importingClassMetadata.getAnnotationTypes() = " + importingClassMetadata.getAnnotationTypes());
return new String[]{CommonConfiguration.class.getName(), BarConfiguration.class.getName()};
}
}
然後把
@EnableCommon
注解擴充一下:
/**
* @author felord.cn
* @since 10:35
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CommonImportSelector.class)
public @interface EnableCommon {
boolean isBar() default false;
}
最後标記在Spring Boot啟動類上:
@EnableCommon
@EnableAsync
@SpringBootApplication
public class SpringSelectorApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSelectorApplication.class, args);
}
}
這裡我特意增加了一個 @EnableAsync
注解來看看能否列印出來。
最後我們寫個測試:
@SpringBootTest
class SpringSelectorApplicationTests {
@Resource
Function<String,Integer> stringLength;
@Resource
FooService fooService;
@Test
void contextLoads() {
Assertions.assertNotNull(stringLength);
Assertions.assertNotNull(fooService);
}
}
經過測試斷言成立,同時控制台将注解中繼資料
importingClassMetadata
的結果列印了出來:
importingClassMetadata.getAnnotationTypes() = [cn.felord.common.EnableCommon,
org.springframework.boot.autoconfigure.SpringBootApplication, org.springframework.scheduling.annotation.EnableAsync]
也就是說
importingClassMetadata
包含了
@Import
所依附的配置類上的所有注解。這意味着我們可以拿到對應注解的元資訊并作為我們動态導入的判斷依據。舉個例子:
/**
* @author felord.cn
* @since 10:19
**/
public class CommonImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 當存在注解 EnableCommon 時 取其useBar 布爾值 true 導入 BarConfiguration
// 其它任何情況将導入 CommonConfiguration 和 BarConfiguration
if (importingClassMetadata.hasAnnotation(EnableCommon.class.getName())) {
MultiValueMap<String, Object> attributes = importingClassMetadata.getAllAnnotationAttributes(EnableCommon.class.getName());
List<Object> useBar = attributes.get("useBar");
boolean userBar = (boolean) useBar.get(0);
if (userBar) {
return new String[]{ BarConfiguration.class.getName()};
}
}
return new String[]{CommonConfiguration.class.getName(), BarConfiguration.class.getName()};
}
}