Spring之@Import

Spring之@Import

前言

在平常開發(fā)中我們自己開發(fā)的組件通常我們可以通過Spring的XML配置文件注解(例如@Component)配置類(例如@Configuration)等方式將組件注入到容器中。但是通常情況下對于第三方開發(fā)的組件,我們很難通過上面的方式來完成。并且想動態(tài)的將組件注冊到容器中,實現(xiàn)起也相對麻煩。例如下面的例子:

public class App {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(GlobalConfig.class);
        //獲取com.buydeem.springimport.UserService實例
        System.out.println(context.getBean(UserService.class));
        //獲取com.buydeem.package2.Person實例
        System.out.println(context.getBean(Person.class));

    }
}

@ComponentScan
@Configuration
class GlobalConfig{

}

@Component
public class Person {
}

@Component
public class UserService {
}
目錄結(jié)果

在上面的代碼示例中,容器中只能獲取到UserService的實例,但是對于Person的實例并不能獲取到。這是因為通常情況下,@ComponentScan注解掃描的包為單前配置類下的包,而Person實例與GlobalConfig并不在同一包下,而對于第三方組件就是這種情況。當(dāng)然我們也可以通過設(shè)置@ComponentScan自定義掃描包來將Person實例注入到容器,不過對于推薦約定大于配置的今天來說,這種方式并不被推薦。

@Import

對于上面的示例中,因為Person類所在的包路徑并不是包掃描的路徑所以無法被注冊到容器中,有沒有什么簡單的方式能將其注入到容器呢?最簡單的方式就是通過@Import注解將Person導(dǎo)入到容器中完成注入。修改代碼如下:

@ComponentScan
@Configuration
@Import(Person.class)
class GlobalConfig{

}

通常情況下,@Import有三種使用方式:

  • 導(dǎo)入一個類作為Spring Bean注冊到容器中
  • @Import注解和ImportSelector組合使用
  • @Import注解和ImportBeanDefinitionRegistrar組合使用

直接導(dǎo)入類注冊到容器

前面示例中我們使用的就是這種方式,通過在注解@Import中設(shè)置導(dǎo)入類,將普通的一個類導(dǎo)入到容器中。不過需要注意的是,在Spring4.2之前是無法將一個普通的類導(dǎo)入到容器中,但是在Spring4.2之后這是允許的,關(guān)于這一點可以參考Spring官方文檔中Using the @Import Annotation中的描述。

@ImportImportSelector組合使用

前面我們說過,@Import方式會更加靈活。但是目前為止,并沒有何處體現(xiàn)出它的靈活之處,而使用ImportSelector我們可以根據(jù)相關(guān)環(huán)境來決定注入哪些類。該接口中只有一個方法selectImports,該返回一個字符串?dāng)?shù)組,數(shù)組中的值則是要注入的Spring Bean。需要注意的一點是,如果沒有需要注入的組件,不能返回null,需要返回一個空的數(shù)組。例如現(xiàn)在有一個這樣的需求,我們需要向容器中注入一個日志類的實現(xiàn),這個日志需要根據(jù)相關(guān)設(shè)置動態(tài)的來注入。我們可以通過ImportSelector來完成。

  • 日志接口和實現(xiàn)
  public interface LogService {
  
      /**
       * 打印日志
       * @param log
       */
      void printLog(String log);
  }
  
  public class LogAServiceImpl implements LogService{
      @Override
      public void printLog(String log) {
          System.out.printf("日志A:[%s]",log);
      }
  }
  
  public class LogBServiceImpl implements LogService{
  
      @Override
      public void printLog(String log) {
          System.out.printf("日志B:[%s]",log);
      }
  }
  • LogImportSelectorEnableLog
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Import(value = {LogImportSelector.class})
  public @interface EnableLog {
  
      String value() default "a";
  }
  
  public class LogImportSelector implements ImportSelector {
  
      @Override
      public String[] selectImports(AnnotationMetadata importingClassMetadata) {
          Map<String, Object> map = importingClassMetadata.getAnnotationAttributes(EnableLog.class.getName(), true);
          String value = (String) map.get("value");
          if (Objects.equals(value,"a")){
              return new String[]{LogAServiceImpl.class.getName()};
          }
          return new String[]{LogBServiceImpl.class.getName()};
      }
  }
  • 測試實例類
  @EnableLog(value = "b")
  public class App {
      public static void main(String[] args) {
          ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
  
          LogService logService = context.getBean(LogService.class);
  
          logService.printLog("日志內(nèi)容");
      }
  }

運行上面的代碼,如果@EnableLog中的值為a時打印的結(jié)果為:

日志A:[日志內(nèi)容]

如果@EnableLog中的值為b時打印的結(jié)果為:

日志B:[日志內(nèi)容]

@EnableXXX的秘密

上面的示例代碼中,LogImportSelector#selectImports()方法通過AnnotationMetadata獲取到注解EnableLog中的值,根據(jù)這個值的配置來動態(tài)的確認(rèn)注入哪個LogService的實現(xiàn),而這種方式就是Spring中@EnableXXX的實現(xiàn)。例如@EnableAsync注解,一般在方法上面加上@Async注解,就可以讓這個方法變成異步執(zhí)行(簡單的說就是使用線程中的一個線程來執(zhí)行,而不是調(diào)用該方法的線程)。不過想要實現(xiàn)這樣的效果,前提條件是需要使用@EnableAsync開啟,下面我們來看看源碼中是如何實現(xiàn)的。

AsyncConfigurationSelector

下面是AdviceModeImportSelector#selectImports()方法的實現(xiàn)

public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
    //獲取到注解
   Class<?> annoType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class);
    //獲取注解里面的值
   AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
   if (attributes == null) {
      throw new IllegalArgumentException(String.format(
         "@%s is not present on importing class '%s' as expected",
         annoType.getSimpleName(), importingClassMetadata.getClassName()));
   }
    //獲取是基于JDK動態(tài)代理還是基于AspectJ做動態(tài)代理的值
   AdviceMode adviceMode = attributes.getEnum(this.getAdviceModeAttributeName());
    //該方法由子類實現(xiàn)
   String[] imports = selectImports(adviceMode);
   if (imports == null) {
      throw new IllegalArgumentException(String.format("Unknown AdviceMode: '%s'", adviceMode));
   }
   return imports;
}

上面的方法中還有一個抽象方法未實現(xiàn)protected abstract String[] selectImports(AdviceMode adviceMode),我們可以看AsyncConfigurationSelector中對該方法的實現(xiàn)。

    public String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
            case PROXY:
                return new String[] { ProxyAsyncConfiguration.class.getName() };
            case ASPECTJ:
                return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME };
            default:
                return null;
        }
    }

上面代碼的邏輯也比較簡單,就是根據(jù)傳入的AdviceMode枚舉值來判斷是使用哪種動態(tài)代理實現(xiàn)方式,從而注入哪個類到容器中。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
    Class<? extends Annotation> annotation() default Annotation.class;
    boolean proxyTargetClass() default false;
    AdviceMode mode() default AdviceMode.PROXY;
    int order() default Ordered.LOWEST_PRECEDENCE;
}

EnableAsync注解可以知道,在默認(rèn)情況下AdviceMode的默認(rèn)值為AdviceMode.PROXY也就是默認(rèn)情況下是使用JDK動態(tài)代理。所以說默認(rèn)情況下注入的Bean為ProxyAsyncConfiguration。簡單的說就是,當(dāng)你的方法使用了@Async之后,通過容器獲得的Bean不是Bean本身,而是一個經(jīng)過加強(qiáng)后的代理Bean。例如我們可以將LogService#printLog方法使用@Async標(biāo)記,然后從容器中獲取LogService并打印出它的類名,通過類名我們可以知道它是一個代理類。

@ImportImportBeanDefinitionRegistrar組合使用

通過前面的內(nèi)容我們了解到,我們可以通過@Import注解導(dǎo)入一個配置類或者一個ImportSelector子類,同樣還可以導(dǎo)入一個ImportBeanDefinitionRegistrar子類。如果導(dǎo)入的是一個普通類時,容器會創(chuàng)建一個Bean并注冊到容器中。如果導(dǎo)入的是一個ImportSelector子類時,則會創(chuàng)建方法selectImports返回的類集合。而ImportBeanDefinitionRegistrar的功能同樣如此,該接口只有一個方法registerBeanDefinitions,這個方法的特別之處在于該方法的入?yún)⑻峁┝?code>BeanDefinitionRegistry實例,而有了BeanDefinitionRegistry則意味著我們可以通過注冊BeanDefinition的方式向容器中注入Bean。

public interface ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}

簡單應(yīng)用

例如現(xiàn)在我們有一個Dog,我們通過ImportBeanDefinitionRegistrar導(dǎo)入的方式來完成注入。

//Dog類
public class Dog {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
//ImportBeanDefinitionRegistrar實現(xiàn)類
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AbstractBeanDefinition dogBd = BeanDefinitionBuilder.genericBeanDefinition(Dog.class)
                .addPropertyValue("name", "大黃")
                .getBeanDefinition();
        registry.registerBeanDefinition("dog",dogBd);
    }
}
//使用示例
@Import(value = MyImportBeanDefinitionRegistrar.class)
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
        Dog dog = context.getBean(Dog.class);
        System.out.println(dog.getName());
    }
}

運行代碼結(jié)果打出我們預(yù)想中的結(jié)果大黃。之所以我們能從容器中獲取到Dog實例,是因為MyImportBeanDefinitionRegistrar#registerBeanDefinitions方法向容器中注冊了DogBeanDefinition

進(jìn)階應(yīng)用

從上面的示例中來看,并沒有體現(xiàn)出什么高級靈活的地方。假如現(xiàn)在我們有這樣一個需求,我們需要把某個包下類都注入到容器中,同時使用一個注解來標(biāo)記只有類上帶有這個標(biāo)記的類我們才注入,并不是所有的類都注入到容器中。面對這種自定義的注入需求,使用ImportBeanDefinitionRegistrar就能很輕松的完成我們的需求。我們將實現(xiàn)分為如下幾步:

定義開啟掃描包的注解和標(biāo)記注解
//該注解用來指定掃描包
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(value = {PackageImportBeanDefinitionRegistrar.class})
public @interface PackageScan {

    String[] basePackages() default {};
}
//該注解用來指定哪些類需要被注入到容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomInjection {

}
實現(xiàn)掃描并注入功能
public class PackageImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        //獲取注解上的屬性
        Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(PackageScan.class.getName());
        //掃描包
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        //添加過濾器掃描含有注解CustomInjection的類
        provider.addIncludeFilter(new AnnotationTypeFilter(CustomInjection.class));
        //掃描包下的BeanDefinition
        String[] basePackages = (String[]) attributes.get("basePackages");
        Set<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
            Set<BeanDefinition> components = provider.findCandidateComponents(basePackage);
            candidateComponents.addAll(components);
        }
        //注冊BeanDefinition
        for (BeanDefinition component : candidateComponents) {
            beanDefinitionRegistry.registerBeanDefinition(component.getBeanClassName(),component);
        }
    }
}

上面代碼的整體實現(xiàn)邏輯比較簡單,首先是獲取到PackageScan中需要指定的包路名集合,接著就是使用ClassPathScanningCandidateComponentProvider獲取到包包中的BeanDefinition,最后通過BeanDefinitionRegistry將找到的BeanDefinition注入到容器。可能難理解的就是獲取包下BeanDefinition時使用的ClassPathScanningCandidateComponentProvider,該類由Spring提供的一個工具類,它的主要功能就是可以幫助我們從包路徑中獲取到所需的 BeanDefinition 集合。

@MapperScans的實現(xiàn)

在使用Mybatis+Spring集成時我們會用到一個工具包mybatis-spring,在該工具包中提供了一個@MapperScans注解,通過該注解我們可以指定掃描包下的Mapper注入到Spring容器中。該注解的實現(xiàn)其實也是借助了@ImportImportBeanDefinitionRegistrar,它的源碼如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
    //省略部分內(nèi)容
}

通過注解上的@Import(MapperScannerRegistrar.class)可以知道,它的實現(xiàn)類為MapperScannerRegistrar

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
   //省略部分代碼
  void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
     //省略部分代碼
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
  }

MapperScannerRegistrar內(nèi)部要做的事簡單來說就是將MapperScannerConfigurer注入到Spring容器中。

總結(jié)

通過上面的內(nèi)容我們了解到,@Import主要有三種使用方式,不管哪種方式其主要目的就是為了動態(tài)而靈活的將組建注入到容器中。雖然我們平時很少使用,但是在很多源碼中我們會看到,特別是在SpringBoot中使用的特別多,它的主要使用場景還是在Spring和第三方組建整合的場景。

本文的示例代碼地址:https://gitee.com/zengchao_workspace/spring-import.git

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容