定義/作用
@Import註解只能作用在類上,一種使用場景是在spring註解驅動開發環境下與配置類配合使用的,其作用是引用其他配置類。使得我們可以和早起的基於XML配置文件開發那樣。使用不同的配置類配置不同的內容,比如Mysql數據源配置用一個配置類。Redis數據源配置用一個配置類等。然後使用在註解在一個主配置類中引入這些從配置類,使得配置更加清晰。被引入的類可以不使用@Configuration、@Component註解。
另一種使用 場景是該註解也是一種註冊bean的方案。可以在配置類中使用Import註冊組件。可以配合ImportSelector、ImportBeanDefinitionRegistrar按一定規則進行組件的批量註冊。
源碼:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* 要引入的配置類,也可以引入ImportSelector、ImportBeanDefinitionRegistrar過濾器和註冊器
* 按照一定的規則進行組件的引入。
*/
Class<?>[] value();
}
使用方式:
不成功的情況:
/**
* @author YeHaocong
* @decription 主配置類
*/
@Configuration
public class SpringConfig {
}
/**
* @author YeHaocong
* @decription Mysql數據源配置類
*/
@Configuration
public class MysqlConfig {
@Bean
public DruidDataSource dataSource() throws IOException {
//創建druid數據源
DruidDataSource dataSource = new DruidDataSource();
//加載配置文件,作爲數據源的初始化屬性
Properties properties = PropertiesLoaderUtils.loadAllProperties("daoconfig/datasource-config.properties");
dataSource.setConnectProperties(properties);
//返回dataSource,spring會把他註冊到IOC容器中。
return dataSource;
}
//.....
}
//測試類
public class ImportDemoTest {
//引入主配置類創建容器
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
@Test
public void testImportDemo(){
DataSource dataSource = (DataSource) context.getBean("dataSource");
System.out.println(dataSource);
}
}
配置文件:
執行結果:
分析:因爲容器是引入主配置類創建,而沒有引入mysql數據源配置類,所以不會掃描創建數據源。
使用Import註解解決:
/**
* @author YeHaocong
* @decription 主配置文件
*/
@Configuration
//使用import註解,把其他從配置類引入
@Import({MysqlConfig.class})
public class SpringConfig {
}
/**
* @author YeHaocong
* @decription Mysql數據源配置文件
*/
//從配置類可以不使用@Configuration和Component等註解。
public class MysqlConfig {
@Bean
public DruidDataSource dataSource() throws IOException {
DruidDataSource dataSource = new DruidDataSource();
//加載配置文件,作爲數據源的初始化屬性
Properties properties = PropertiesLoaderUtils.loadAllProperties("daoconfig/datasource-config.properties");
dataSource.setConnectProperties(properties);
//返回dataSource,spring會把他註冊到IOC容器中。
return dataSource;
}
//.....
}
執行結果:
可見數據源配置類被成功引入,數據源成功創建。
被引入的類會被註冊到spring的IOC容器中,並且組件id爲類的全限定名稱,比如上面的:
public class ImportDemoTest {
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
@Test
public void testImportDemo(){
//獲取MysqlConfig配置組件
MysqlConfig mysqlConfig = context.getBean(MysqlConfig.class);
System.out.println(mysqlConfig);
//獲取註解中所有的組件名稱
String[] beanNames = context.getBeanDefinitionNames();
for (String beanName:beanNames)
System.out.println(beanName);
}
}
結果:
ImportSelector和ImportBeanDefinitionRegistrar
我們註冊bean的方式有很多種。
比如:
- 我們自己寫的類,可以使用@Component及其衍生類進行註冊。
- 到導入第三方庫時,可以使用@Bean註解和@Import註解進行註冊。
但是,當要註冊的類很多時,每個類上加註解,寫Bean方法註冊,用Import方法導入大量的Bean時,會顯得很繁瑣,此時可以使用自定義ImportSelector和ImportBeanDefinitionRegistrar來實現組件的批量註冊。spring boot有很多EnableXXX的註解,絕大多數多借助了ImportSelector和ImportBeanDefinitionRegistrar。
共同點:
- 他們都用於動態註冊bean對象到容器中,並且支持大批量的bean導入。
區別:
- ImportSelector是一個接口,我們在使用時需要提供自己的實現類,實現類中重寫的方法返回要註冊的bean的全限定名數組。然後ConfigurationClassParser類中的precessImports方法註冊bean對象。
- ImportBeanDefinitionRegistrar也是一個接口,需要我們自己提供實現類,在實現類中手動註冊bean到容器中。
注意事項:實現了ImportSelector和ImportBeanDefinitionRegistrar的類不會被解析成一個bean添加到容器中。
ImportSelector
demo:
包結構:
代碼:
/**
* @author YeHaocong
* @decription 自定義的ImportSelector,導入選擇器。
* 1. 通過AspectJ表達式進行類型篩選。
* 2. 當使用該選擇器的配置類沒有使用@ComponentScan註解指定掃描包時,會掃描該配置類所在包及其子包。
*/
public class CustomImportSelector implements ImportSelector {
//AspectJ表達式
private String expression;
public CustomImportSelector() throws IOException {
try {
//載入配置文件,創建一個Properties對象
Properties props = PropertiesLoaderUtils.loadAllProperties("import/custom-import-selector.properties");
//獲取配置文件配置的鍵爲 expression的值,並賦值給expression變量
expression = props.getProperty("expression");
if (expression == null || expression.isEmpty()){
throw new RuntimeException("配置文件import/custom-import-selector.properties 的expression 不存在");
}
}
catch (RuntimeException e){
throw e;
}
}
/**
*
* @param importingClassMetadata 參數是被Import註解作用的配置類的註解元信息
* @return 返回的是要註冊的組件的類的全限定名數組。
*/
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//定義要掃描的基礎包
String[] basePackages = null;
//獲取ComponentScan註解的全限定名稱。
String ComponentScanName = ComponentScan.class.getName();
//判斷被Import註解作用的類上是否有@ComponentScan註解
if (importingClassMetadata.hasAnnotation(ComponentScanName)){
//有@ComponentScan註解,獲取該註解上的屬性配置,封裝成Map對象。
Map<String,Object> attributes = importingClassMetadata.getAnnotationAttributes(ComponentScanName);
//獲取@ComponentScan註解的value屬性或者basePackages屬性,因爲他們是互爲別名,所以獲取其中一個即可。
basePackages = (String[]) attributes.get("basePackages");
}
//判斷是否有ComponentScan註解或者ComponentScan註解是否有指定掃描包。
//當basePackages爲null時,表示沒有ComponentScan註解。
//當basePackages.length等於0時,表示有basePackages註解,但是沒有指定掃描的包。
if (basePackages == null || basePackages.length == 0){
//如果@Import註解作用的配置類上沒有ComponentScan註解或者有ComponentScan註解但是沒有指定掃描包的情況下。
//我們就掃描該配置類所在包及其子包。
String basePackage = null;
//獲取被Import註解作用的配置類所在的包。
try {
basePackage = Class.forName(importingClassMetadata.getClass().getName()).getPackage().getName();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//把包名設置到basePackages中。
basePackages = new String[]{basePackage};
}
//創建類路徑掃描器,參數的含義是不使用默認的過濾規則,與@ComponentScan註解的 useDefaultFilters屬性一樣。
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
//創建類型過濾器,此處使用AspectJ類型過濾器。傳入參數是AspectJ表達式和類加載器對象
TypeFilter typeFilter = new AspectJTypeFilter(expression,CustomImportSelector.class.getClassLoader());
//類型過濾器添加到掃描器中。添加的是包含掃描器。
scanner.addIncludeFilter(typeFilter);
//定義要掃描類的全限定類名的集合
Set<String> classes = new HashSet<>();
//遍歷基礎掃描類數組,得到要掃描的類的全限定名,並添加到集合中
for (String basePackage: basePackages){
//掃描基礎包,獲取掃描到的BeanDefinition集合
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
//遍歷。獲取全限定名添加到集合中。
for (BeanDefinition beanDefinition: candidateComponents){
classes.add(beanDefinition.getBeanClassName());
}
}
//返回集合
return classes.toArray(new String[classes.size()]);
}
}
/**
*配置類
*/
@Configuration
@Import({CustomImportSelector.class})
public class SpringConfig {
}
//還有兩個業務接口和兩個業務接口實現類和一個ConfigUtil,這兩個業務實現類和ConfigUtil類都是要註冊的組件。這裏不再貼出,可以看上面包結構。
public class TestImportSelector {
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
@Test
public void TestImportSelector(){
//根據類型獲取bean
try {
ConfigUtil configUtil = (ConfigUtil) context.getBean(ConfigUtil.class);
System.out.println(configUtil);
}catch (Exception e){
System.out.println(e.getMessage());
}
try {
RoleService roleService = (RoleService) context.getBean(RoleService.class);
System.out.println(roleService);
}catch (Exception e){
System.out.println(e.getMessage());
}
try {
UserService userService = (UserService) context.getBean(UserService.class);
System.out.println(userService);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
配置文件:
執行結果:
分析:一個bean都沒有註冊成功,原因是:
配置類SpringConfig上沒有使用@ComponentScan或者使用了但是沒有配置掃描包。所以會掃描配置類所在包及其子包,看上面包結果。明顯沒有掃描到service包。所以兩個業務實現類沒有被註冊到容器中。而ConfigUtil雖然被掃描到了,但是由於不符合AspectJ表達式而沒有被添加到選擇器中。
接下來我們使用@ComponentScan掃描指定包。
@Configuration
@ComponentScan(basePackages = "importselectdemo")
@Import({CustomImportSelector.class})
public class SpringConfig {
}
/**
*測試類
*/
public class TestImportSelector {
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
@Test
public void TestImportSelector(){
//根據類型獲取bean
try {
ConfigUtil configUtil = (ConfigUtil) context.getBean(ConfigUtil.class);
System.out.println(configUtil);
}catch (Exception e){
System.out.println(e.getMessage());
}
try {
RoleService roleService = (RoleService) context.getBean(RoleService.class);
System.out.println(roleService);
}catch (Exception e){
System.out.println(e.getMessage());
}
try {
UserService userService = (UserService) context.getBean(UserService.class);
System.out.println(userService);
}catch (Exception e){
System.out.println(e.getMessage());
}
try {
CustomImportSelector selector = (CustomImportSelector) context.getBean(CustomImportSelector.class);
System.out.println(selector);
}catch (Exception e){
System.out.println(e.getMessage());
}
String[] beanNames = context.getBeanDefinitionNames();
for (String beanName:beanNames){
System.out.println(beanName);
}
}
}
執行結果:
注意:不能導入配置類自身,因爲,這樣會導致報錯。
將表達式設置爲:
結果:
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: A circular @Import has been detected: Illegal attempt by @Configuration class 'SpringConfig' to import class 'SpringConfig' as 'SpringConfig' is already present in the current import stack [SpringConfig->SpringConfig]
Offending resource: importselectdemo.config.SpringConfig
使用上述方法只要符合CustomImportSelector規則,即使不使用@Component等註解也會註冊到容器中。
ImportBeanDefinitionRegistrar
這個註冊器不會把掃描到的類返回,而是把掃描到的類直接就在這裏註冊了。
demo(掃描邏輯與上面的CustomImportSelector一樣):
/**
* @author YeHaocong
* @decription 自定義的ImportBeanDefinitionRegistrar,導入註冊器。
* 1. 通過AspectJ表達式進行類型篩選。
* 2. 當使用該選擇器的配置類沒有使用@ComponentScan註解指定掃描包時,會掃描該配置類所在包及其子包。
* 3. CustomImportDefinitionRegistrar會掃描指定包裏,符合AspectJ表達式的組件的類進行註冊
*/
public class CustomImportDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
//AspectJ表達式
private String expression;
public CustomImportDefinitionRegistrar() throws IOException {
try {
//載入配置文件,創建一個Properties對象
Properties props = PropertiesLoaderUtils.loadAllProperties("import/custom-import-selector.properties");
//獲取配置文件配置的鍵爲 expression的值,並賦值給expression變量
expression = props.getProperty("expression");
if (expression == null || expression.isEmpty()){
throw new RuntimeException("配置文件import/custom-import-selector.properties 的expression 不存在");
}
}
catch (RuntimeException e){
throw e;
}
}
/**
*
* @param importingClassMetadata 參數是被Import註解作用的配置類的註解元信息
* @param registry BeanDefinition註冊器,會將掃描到的類直接使用該註冊器進行註冊
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//定義要掃描的基礎包
String[] basePackages = null;
//獲取ComponentScan註解的全限定名稱。
String ComponentScanName = ComponentScan.class.getName();
//判斷被Import註解作用的類上是否有@ComponentScan註解
if (importingClassMetadata.hasAnnotation(ComponentScanName)){
//有@ComponentScan註解,獲取該註解上的屬性配置,封裝成Map對象。
Map<String,Object> attributes = importingClassMetadata.getAnnotationAttributes(ComponentScanName);
//獲取@ComponentScan註解的value屬性或者basePackages屬性,因爲他們是互爲別名,所以獲取其中一個即可。
basePackages = (String[]) attributes.get("basePackages");
}
//判斷是否有ComponentScan註解或者ComponentScan註解是否有指定掃描包。
//當basePackages爲null時,表示沒有ComponentScan註解。
//當basePackages.length等於0時,表示有basePackages註解,但是沒有指定掃描的包。
if (basePackages == null || basePackages.length == 0){
//如果@Import註解作用的配置類上沒有ComponentScan註解或者有ComponentScan註解但是沒有指定掃描包的情況下。
//我們就掃描該配置類所在包及其子包。
String basePackage = null;
//獲取被Import註解作用的配置類所在的包。
try {
basePackage = Class.forName(importingClassMetadata.getClass().getName()).getPackage().getName();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//把包名設置到basePackages中。
basePackages = new String[]{basePackage};
}
//創建類路徑掃描器ClassPathBeanDefinitionScanner,參數的含義是不使用默認的過濾規則,與@ComponentScan註解的 useDefaultFilters屬性一樣。
//registry參數是將掃描到的類使用指定的registry註冊器註冊
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry,false);
//創建類型過濾器,此處使用AspectJ類型過濾器。傳入參數是AspectJ表達式和類加載器對象
TypeFilter typeFilter = new AspectJTypeFilter(expression,CustomImportSelector.class.getClassLoader());
//類型過濾器添加到掃描器中。添加的是包含掃描器。
scanner.addIncludeFilter(typeFilter);
//進行掃描
scanner.scan(basePackages);
}
}
//配置類:
@Configuration
@ComponentScan(basePackages = "importselectdemo")
//使用CustomImportDefinitionRegistrar
@Import({CustomImportDefinitionRegistrar.class})
public class SpringConfig {
}
結果:
解析:
- ConfigUtil不符合AspectJ表達式規則,所以沒有註冊。
- 業務類註冊成功。
- 實現ImportBeanDefinitionRegistrar接口的類不會被添加到容器中。
- 因爲使用的是BeanDefinitionRegistry註冊器,所以註冊的bean id 默認是類的名字第一個轉小寫。而不是全限定名稱。
使用上述方法只要符合CustomImportDefinitionRegistrar規則,即使不使用@Component等註解也會註冊到容器中。