Spring之@Import

Spring之@Import

前言

在平??⒅形颐亲约嚎⒌淖榧ǔN颐强梢酝ü齋pring的XML配置文件,注解(例如@Component)配置类(例如@Configuration)等方式将组件注入到容器中。但是通常情况下对于第三方开发的组件,我们很难通过上面的方式来完成。并且想动态的将组件注册到容器中,实现起也相对麻烦。例如下面的例子:

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 {
}
目录结果

在上面的代码示例中,容器中只能获取到UserService的实例,但是对于Person的实例并不能获取到。这是因为通常情况下,@ComponentScan注解扫描的包为单前配置类下的包,而Person实例与GlobalConfig并不在同一包下,而对于第三方组件就是这种情况。当然我们也可以通过设置@ComponentScan自定义扫描包来将Person实例注入到容器,不过对于推荐约定大于配置的今天来说,这种方式并不被推荐。

@Import

对于上面的示例中,因为Person类所在的包路径并不是包扫描的路径所以无法被注册到容器中,有没有什么简单的方式能将其注入到容器呢?最简单的方式就是通过@Import注解将Person导入到容器中完成注入。修改代码如下:

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

}

通常情况下,@Import有三种使用方式:

  • 导入一个类作为Spring Bean注册到容器中
  • @Import注解和ImportSelector组合使用
  • @Import注解和ImportBeanDefinitionRegistrar组合使用

直接导入类注册到容器

前面示例中我们使用的就是这种方式,通过在注解@Import中设置导入类,将普通的一个类导入到容器中。不过需要注意的是,在Spring4.2之前是无法将一个普通的类导入到容器中,但是在Spring4.2之后这是允许的,关于这一点可以参考Spring官方文档中Using the @Import Annotation中的描述。

@ImportImportSelector组合使用

前面我们说过,@Import方式会更加灵活。但是目前为止,并没有何处体现出它的灵活之处,而使用ImportSelector我们可以根据相关环境来决定注入哪些类。该接口中只有一个方法selectImports,该返回一个字符串数组,数组中的值则是要注入的Spring Bean。需要注意的一点是,如果没有需要注入的组件,不能返回null,需要返回一个空的数组。例如现在有一个这样的需求,我们需要向容器中注入一个日志类的实现,这个日志需要根据相关设置动态的来注入。我们可以通过ImportSelector来完成。

  • 日志接口和实现
  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("日志内容");
      }
  }

运行上面的代码,如果@EnableLog中的值为a时打印的结果为:

日志A:[日志内容]

如果@EnableLog中的值为b时打印的结果为:

日志B:[日志内容]

@EnableXXX的秘密

上面的示例代码中,LogImportSelector#selectImports()方法通过AnnotationMetadata获取到注解EnableLog中的值,根据这个值的配置来动态的确认注入哪个LogService的实现,而这种方式就是Spring中@EnableXXX的实现。例如@EnableAsync注解,一般在方法上面加上@Async注解,就可以让这个方法变成异步执行(简单的说就是使用线程中的一个线程来执行,而不是调用该方法的线程)。不过想要实现这样的效果,前提条件是需要使用@EnableAsync开启,下面我们来看看源码中是如何实现的。

AsyncConfigurationSelector

下面是AdviceModeImportSelector#selectImports()方法的实现

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动态代理还是基于AspectJ做动态代理的值
   AdviceMode adviceMode = attributes.getEnum(this.getAdviceModeAttributeName());
    //该方法由子类实现
   String[] imports = selectImports(adviceMode);
   if (imports == null) {
      throw new IllegalArgumentException(String.format("Unknown AdviceMode: '%s'", adviceMode));
   }
   return imports;
}

上面的方法中还有一个抽象方法未实现protected abstract String[] selectImports(AdviceMode adviceMode),我们可以看AsyncConfigurationSelector中对该方法的实现。

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

上面代码的逻辑也比较简单,就是根据传入的AdviceMode枚举值来判断是使用哪种动态代理实现方式,从而注入哪个类到容器中。

@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注解可以知道,在默认情况下AdviceMode的默认值为AdviceMode.PROXY也就是默认情况下是使用JDK动态代理。所以说默认情况下注入的Bean为ProxyAsyncConfiguration。简单的说就是,当你的方法使用了@Async之后,通过容器获得的Bean不是Bean本身,而是一个经过加强后的代理Bean。例如我们可以将LogService#printLog方法使用@Async标记,然后从容器中获取LogService并打印出它的类名,通过类名我们可以知道它是一个代理类。

@ImportImportBeanDefinitionRegistrar组合使用

通过前面的内容我们了解到,我们可以通过@Import注解导入一个配置类或者一个ImportSelector子类,同样还可以导入一个ImportBeanDefinitionRegistrar子类。如果导入的是一个普通类时,容器会创建一个Bean并注册到容器中。如果导入的是一个ImportSelector子类时,则会创建方法selectImports返回的类集合。而ImportBeanDefinitionRegistrar的功能同样如此,该接口只有一个方法registerBeanDefinitions,这个方法的特别之处在于该方法的入参提供了BeanDefinitionRegistry实例,而有了BeanDefinitionRegistry则意味着我们可以通过注册BeanDefinition的方式向容器中注入Bean。

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

简单应用

例如现在我们有一个Dog,我们通过ImportBeanDefinitionRegistrar导入的方式来完成注入。

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
//ImportBeanDefinitionRegistrar实现类
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());
    }
}

运行代码结果打出我们预想中的结果大黄。之所以我们能从容器中获取到Dog实例,是因为MyImportBeanDefinitionRegistrar#registerBeanDefinitions方法向容器中注册了DogBeanDefinition。

进阶应用

从上面的示例中来看,并没有体现出什么高级灵活的地方。假如现在我们有这样一个需求,我们需要把某个包下类都注入到容器中,同时使用一个注解来标记只有类上带有这个标记的类我们才注入,并不是所有的类都注入到容器中。面对这种自定义的注入需求,使用ImportBeanDefinitionRegistrar就能很轻松的完成我们的需求。我们将实现分为如下几步:

定义开启扫描包的注解和标记注解
//该注解用来指定扫描包
@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 {

}
实现扫描并注入功能
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);
        }
    }
}

上面代码的整体实现逻辑比较简单,首先是获取到PackageScan中需要指定的包路名集合,接着就是使用ClassPathScanningCandidateComponentProvider获取到包包中的BeanDefinition,最后通过BeanDefinitionRegistry将找到的BeanDefinition注入到容器。可能难理解的就是获取包下BeanDefinition时使用的ClassPathScanningCandidateComponentProvider,该类由Spring提供的一个工具类,它的主要功能就是可以帮助我们从包路径中获取到所需的 BeanDefinition 集合。

@MapperScans的实现

在使用Mybatis+Spring集成时我们会用到一个工具包mybatis-spring,在该工具包中提供了一个@MapperScans注解,通过该注解我们可以指定扫描包下的Mapper注入到Spring容器中。该注解的实现其实也是借助了@ImportImportBeanDefinitionRegistrar,它的源码如下:

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

通过注解上的@Import(MapperScannerRegistrar.class)可以知道,它的实现类为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内部要做的事简单来说就是将MapperScannerConfigurer注入到Spring容器中。

总结

通过上面的内容我们了解到,@Import主要有三种使用方式,不管哪种方式其主要目的就是为了动态而灵活的将组建注入到容器中。虽然我们平时很少使用,但是在很多源码中我们会看到,特别是在SpringBoot中使用的特别多,它的主要使用场景还是在Spring和第三方组建整合的场景。

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

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容