SpringBoot 下使用 @Import、ImportSelector 实现自定义规则 Bean 注册

1 Spring 内置的扫描器

使用XML 配置 Spring 组件扫描使用的是 component-scan 标签,其底层使用 ClassPathBeanDefinitionScanner 这个类完成扫描工作的。
到了 SpringBoot 这里,可以使用注解 @ComponentScan 注解配合 @Configuration@Service@Repository@Controller 等注解使用,底层则使用了 ComponentScanAnnotationParser 解析器完成解析工作。

1.1 实现原理

ComponentScanAnnotationParser 解析器内部使用了 ClassPathBeanDefinitionScanner 扫描器.
ClassPathBeanDefinitionScanner扫描器内部的处理过程整理如下:

遍历 basePackages,找出这个包下的所有的 class。找出之后封装成 Resource 接口集合,这个 Resource 接口是 Spring 对资源的封装,有 FileSystemResource、ClassPathResource、UrlResource 等实现。遍历找到的 Resource 集合,通过 includeFiltersexcludeFilters 判断是否解析。这里的 includeFiltersexcludeFiltersTypeFilter 接口类型的集合,是 ClassPathBeanDefinitionScanner 内部的属性。
TypeFilter 接口是一个用于判断类型是否满足要求的类型过滤器。excludeFilters 中只要有一个TypeFilter满足条件,这个Resource就会被过滤。includeFilters 中只要有一个TypeFilter满足条件,这个Resource就会被保留,最后把保留的 Resource 封装成 ScannedGenericBeanDefinition 添加到 BeanDefinition 结果集中。

TypeFilter接口的定义:

public interface TypeFilter {
	boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
			throws IOException;
}

TypeFilter 常用实现:

  • AnnotationTypeFilter:类是否有注解修饰
  • RegexPatternTypeFilter:类名是否满足正则表达式。

ClassPathBeanDefinitionScanner 继承 ClassPathScanningCandidateComponentProvider

ClassPathScanningCandidateComponentProvider 内部的构造函数提供了一个 useDefaultFilters 参数,这个参数表示是否使用默认的TypeFilter,如果设置为true,会添加默认的TypeFilter:

public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) {
  this(useDefaultFilters, new StandardEnvironment());
}

// 默认 TypeFilter
protected void registerDefaultFilters() {
	  this.includeFilters.add(new AnnotationTypeFilter(Component.class));
	  ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
	  try {
	    this.includeFilters.add(new AnnotationTypeFilter(
	        ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
	    logger.debug("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
	  }catch (ClassNotFoundException ex) {
	    // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
	  }
	  try {
	    this.includeFilters.add(new AnnotationTypeFilter(
	        ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
	    logger.debug("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
	  }catch (ClassNotFoundException ex) {
	    // JSR-330 API not available - simply skip.
	  }
}

可以看到这里 includeFilters 加上了 AnnotationTypeFilter,并且对应的注解是 @Component。@Service、@Controller或@Repository。(这几个注解内部都是被@Component注解所修饰)。

2 @Import 介绍

SpringBoot 中,如果无脑扫描 @Component 及其衍生注解修饰的类的话,使用 @ComponentScan 注解就可以完成,但如果要控制一下扫描细节的话,就得自己实现了。
@Import是原生 Spring 的一个注解,可以将@Configuration标记的类、ImportSelector的实现类以及ImportBeanDefinitionRegistrar的实现类导入。在Spring 4.2版本以后,普通的类也可以被导入,将其注册到 Spring IOC 容器中。
在 @Import 注解的属性中可以设置需要引入的类名,例如 @AutoConfigurationPackage 注解上的 @Import(AutoConfigurationPackages.Registrar.class)。根据该类的不同类型,Spring 容器针对 @Import 注解有以下四种处理方式:

  • 如果该类实现了 ImportSelector 接口,Spring 容器就会实例化该类,并且调用其 selectImports 方法;

  • 如果该类实现了 DeferredImportSelector 接口,则 Spring 容器也会实例化该类并调用其 selectImports方法。DeferredImportSelector 继承了 ImportSelector,区别在于 DeferredImportSelector 实例的 selectImports 方法调用时机晚于 ImportSelector 的实例,要等到 @Configuration 注解中相关的业务全部都处理完了才会调用;

  • 如果该类实现了 ImportBeanDefinitionRegistrar 接口,Spring 容器就会实例化该类,并且调用其 registerBeanDefinitions 方法;

  • 如果该类没有实现上述三种接口中的任何一个,Spring 容器就会直接实例化该类。

参考SpringBoot源码理解:AutoConfigurationPackages.RegistrarAutoConfigurationImportSelector

如果是注册明确的有限个数的类,使用 @Import 直接导入即可:

@Import({TestBean1.class})   // TestBean1 就是一个普通的 java bean
@Configuration
public class AppConfig {
}

导入使用 @Configuration 修饰的类

@Configuration
public class TestConfig {
    @Bean
    public TestBean2 getTestBean2(){
        return new TestBean2();
    }
}

@Import({TestConfig.class})
@Configuration
public class AppConfig {}

导入一个实现了ImportSelector接口的类,实现接口selectImports方法,返回一个包含了类全限定名的数组:

public class TestImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      	// AnnotationMetadata 是注解启动类的上的注解元信息
        return new String[]{"com.example.demo.bean.TestBean3"};
    }
  
  /**
    * 可选的实现方法
    * 返回一个谓词接口,该方法制定了一个对类全限定名的排除规则来过滤一些候选的导入类,默认不排除过滤。
    *
    * @since 5.2.4
    */
   public Predicate<String> getExclusionFilter() {
      return null;
   }
}


@Import({TestImportSelector.class})
@Configuration
public class AppConfig {
}

3 综合使用实现自定义扫描功能

上面 @import 的部分中实现了大部分场景下的 Bean 注入功能,但如果一个场景中,需要批量注入一些特定的类的话,就要借助 Spring 的扫描组件来实现了。
一般情况下,我们要自定义扫描功能的话,可以直接使用 ClassPathScanningCandidateComponentProvider 完成扫描过程,加上一些自定义的TypeFilter 即可。或者写个自定义扫描器继承ClassPathScanningCandidateComponentProvider,并在内部添加自定义的 TypeFilter

我们就以一个简单的例子说明一下自定义扫描的实现,直接使用ClassPathScanningCandidateComponentProvider。
两个使用了自定义注解并且都实现了 IChanel 接口的类,要把这两类扫描注册到 Spring 容器中:

package org.extra.bean;

@Channel("channel_one")
public class ChannelOne implements IChannel {
    @Override
    public void say() {
        System.out.println("I am one");
    }
}

@Channel("channel_two")
public class ChannelTwo implements IChannel {
    @Override
    public void say() {
        System.out.println("I am two");
    }
}

实现扫描规则:

public class ChannelLoadPolicy implements ImportSelector {

    private final String BASE_PACKAGE = "org.extra.bean";

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 不使用默认的TypeFilter
        ClassPathScanningCandidateComponentProvider provider = 
        								new ClassPathScanningCandidateComponentProvider(false);
        // 添加扫描规律规则,这里指定了内置的注解过滤规则
        provider.addIncludeFilter(new AnnotationTypeFilter(Channel.class));
        // 扫描指定包,如果有多个包,这个过程可以执行多次
        Set<BeanDefinition> beanDefinitionSet = provider.findCandidateComponents(BASE_PACKAGE);

		// 获取扫描结果的全限定类名
        List<String> className = new ArrayList<>();
        beanDefinitionSet.forEach(beanDefinition -> className.add(beanDefinition.getBeanClassName()));

        String[] classNameArray = new String[className.size()];
        String[] array = className.toArray(classNameArray);

        return array;
    }
}

编写一个开关注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ChannelLoadPolicy.class)   // 指定了上面的扫描规则类
public @interface EnableChannel {
}

在 SpringBoot 启动类中使用这个开关:

@SpringBootApplication
@EnableChannel
public class OSSApp {}

自定义扫描器完成。

——————————
总结:上面的几种方式,几乎能处理绝大多数的自定义扫描场景了,极特殊场景,也可以借助以上的思路和 Spring 提供的工具来实现自己的解决方案,如果是非 Spring 环境下,也可以参考 Spring 的源码,实现一个纯手工的扫描过程,也不是太难的事。


版权声明:本文为zombres原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>