Spring里面的Import注解到底是个啥?

1,445 阅读3分钟

 Spring提供了几种方式来注册Bean,日常开发中使用最多的是ComponentScan。得益于ComponentScan,注册bean非常的简单,只需要在被注册的类上声明@Component或者@Service等注解即可。

 除了ComponentScan,Spring还支持使用Configuration注解来注册Bean。在大型的项目中,模块化开发能极大地降低系统的复杂性,这时需要每个模块来定义本模块Bean注册情况,Configuration发挥着巨大的作用。

@Configuration
@ConditionalOnProperty(prefix = "module.wx-login", value = "enable", havingValue = "true")
@ComponentScan(basePackages = "com.lin.decorator.wxlogin")
public class WxLoginConfiguration {
}

 每个模块定义了Configuration之后,需要将多个模块的Configuration组合。Spring提供了Import注解来实现多个Configuration组合。

@Import(WxLoginConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 Spring官方文档中关于Import的描述如下:

Provides functionality equivalent to the <import/> element in Spring XML. Allows for importing @Configuration classes, ImportSelector and ImportBeanDefinitionRegistrar implementations, as well as regular component classes (as of 4.2; analogous to AnnotationConfigApplicationContext.register(java.lang.Class<?>...)). @Bean definitions declared in imported @Configuration classes should be accessed by using @Autowired injection. Either the bean itself can be autowired, or the configuration class instance declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly navigation between @Configuration class methods.

 除了Configuration,Import还支持引入ImportSelector和ImportBeanDefinitionRegistrar。既然要全面了解Import机制,那另外两个也要一探究竟。

ImportSelector

 Spring官方文档中,对ImportSelector的描述如下: Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

 从字面上理解,ImportSelector可以根据注解里面的一个或多个属性来决定引入哪些Configuration。举个例子:

 小伙伴都用过Transactional注解,Transactional注解生效的前提是EnableTransactionManagement生效。看过EnableTransactionManagement源代码的小伙伴应该都知道,它通过Import引入了一个ImportSelector。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
   boolean proxyTargetClass() default false;
   AdviceMode mode() default AdviceMode.PROXY;
   int order() default Ordered.LOWEST_PRECEDENCE;
}

 而TransactionManagementConfigurationSelector会根据注解里面的AdviceMode不同,来确定引入不同的Configuration。

protected String[] selectImports(AdviceMode adviceMode) {
    switch (adviceMode) {
        case PROXY:
            return new String[] {AutoProxyRegistrar.class.getName(),
               ProxyTransactionManagementConfiguration.class.getName()};
      case ASPECTJ:
          return new String[] {determineTransactionAspectClass()};
      default:
          return null;
    }
}

ImportBeanDefinitionRegistrar

 Spring官方文档中,对ImportBeanDefinitionRegistrar的描述如下: Interface to be implemented by types that register additional bean definitions when processing @Configuration classes. Useful when operating at the bean definition level (as opposed to @Bean method/instance level) is desired or necessary.

 字面意思是,通过继承这个接口可以额外定义Bean。举个例子:

 在使用Mybatis的时候,会使用到MapperScan这个注解,这个注解通过Import引入了ImportBeanDefinitionRegistrar,这也解释了为什么我们只在Interface上申明了一个Mapper,mybatis就帮我们生成好了Bean。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}

 有小伙伴在编码过程中,并没有使用MapperScan,为什么也能正常使用呢?其实是Mybatis starter的功劳。在MybatisAutoConfiguration里面定义了ImportBeanDefinitionRegistrar,当MapperScan没有激活时,它就会生效。

@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan," +
           "MapperFactoryBean and MapperScannerConfigurer.");
    }
  }

Import执行流程

 了解了Import支持的三种不同类型的资源之后,接下来debug看一下import的执行过程。通过设置断点,发现在ConfigurationClassParser类中,通过深度遍历来处理Import。

private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
         throws IOException {

    if (visited.add(sourceClass)) {
        for (SourceClass annotation : sourceClass.getAnnotations()) {
            String annName = annotation.getMetadata().getClassName();
            if (!annName.equals(Import.class.getName())) {
                collectImports(annotation, imports, visited);
            }
        }
        imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
    }
}

 而上面介绍的ImportSelector,需要调用selectImports方法进行解析。

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
         Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
         boolean checkForCircularImports) {

      if (importCandidates.isEmpty()) {
         return;
      }

      if (checkForCircularImports && isChainedImportOnStack(configClass)) {
         this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
      }
      else {
         this.importStack.push(configClass);
         try {
            for (SourceClass candidate : importCandidates) {
               if (candidate.isAssignable(ImportSelector.class)) {
                  // Candidate class is an ImportSelector -> delegate to it to determine imports
                  Class<?> candidateClass = candidate.loadClass();
                  ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
                        this.environment, this.resourceLoader, this.registry);
                  Predicate<String> selectorFilter = selector.getExclusionFilter();
                  if (selectorFilter != null) {
                     exclusionFilter = exclusionFilter.or(selectorFilter);
                  }
                  if (selector instanceof DeferredImportSelector) {
                     this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                  }
                  else {
                     String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                     Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
                     processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
                  }
               }
               else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                  // Candidate class is an ImportBeanDefinitionRegistrar ->
                  // delegate to it to register additional bean definitions
                  Class<?> candidateClass = candidate.loadClass();
                  ImportBeanDefinitionRegistrar registrar =
                        ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                              this.environment, this.resourceLoader, this.registry);
                  configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
               }
               else {
                  // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                  // process it as an @Configuration class
                  this.importStack.registerImport(
                        currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                  processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
               }
            }
         }
         catch (BeanDefinitionStoreException ex) {
            throw ex;
         }
         catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                  "Failed to process import candidates for configuration class [" +
                  configClass.getMetadata().getClassName() + "]", ex);
         }
         finally {
            this.importStack.pop();
         }
      }
   }
这样递归调用,就实现了资源的加载。