logo头像

From zero to HERO

使用Spring注解@Import进行Bean的导入管理

1. 前言

很多时候我们的Spring项目使用多模块,或者我们需要将自己特定的类库打成依赖。默认情况下Spring Boot应用只会扫描main方法所在的包路径下的Bean和通过spring.factories进行注册发现自动装配到Spring IoC中去。像下面这个Maven项目中,如果Spring BootMain类在cn.felord.yaml包下的话cn.felord.common包的Spring Bean是无法被扫描注册到Spring IoC容器中的。

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

另外还有一个ImportSelector的变种接口DeferredImportSelector。 它的特点是所有的配置(@Configuration)类都处理完才进行选择导入,而ImportSelector正相反。另外 DeferredImportSelector还提供了分组过滤、排序的功能。在导入条件配置@Conditional时特别有用。

4. 总结

@Import注解的相关系列非常有用,特别是项目分包,多模块之间的Spring Bean管理,自定义Spring Boot Starter等场景中。多多关注:码农小胖哥 获取更多干货。如果有问题你也可以加微信MSW_623进行探讨。

评论系统未开启,无法评论!