天天看點

Spring @Import 注解妙用

1. 前言

很多時候我們的Spring項目使用多子產品,或者我們需要将自己特定的類庫打成依賴。預設情況下Spring Boot應用隻會掃描main方法所在的包路徑下的Bean和通過​

​spring.factories​

​進行注冊發現自動裝配到Spring IoC中去。像下面這個Maven項目中,如果Spring Boot的Main類在​

​cn.felord.yaml​

​​包下的話​

​cn.felord.common​

​包的Spring Bean是無法被掃描注冊到Spring IoC容器中的。

Spring @Import 注解妙用

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()};
    }
}      

4. 總結