<导航

spring之@import与ImportSelector

1、@Import 

在spring中@Component 及其衍生注解修饰的类的,使用 @ComponentScan 注解就可以将其注入到springioc容器,但如果要控制一下扫描细节的话,就得自己实现了。

@Import是原生 Spring 的一个注解,可以将@Configuration标记的类、ImportSelector的实现类以及ImportBeanDefinitionRegistrar的实现类导入。在Spring 4.2版本以后,普通的类也可以被导入,将其注册到 Spring IOC 容器中。
在 @Import 注解的属性中可以设置需要引入的类名。根据该类的不同类型,Spring 容器针对 @Import 注解有以下四种处理方式:

  • 如果该类实现了 ImportSelector 接口,Spring 容器就会实例化该类,并且调用其 selectImports 方法;
  • 如果该类实现了 DeferredImportSelector 接口,则 Spring 容器也会实例化该类并调用其 selectImports方法。DeferredImportSelector 继承了 ImportSelector,区别在于 DeferredImportSelector 实例的 selectImports 方法调用时机晚于 ImportSelector 的实例,要等到 @Configuration 注解中相关的业务全部都处理完了才会调用;
  • 如果该类实现了 ImportBeanDefinitionRegistrar 接口,Spring 容器就会实例化该类,并且调用其 registerBeanDefinitions 方法;
  • 如果该类没有实现上述三种接口中的任何一个,Spring 容器就会直接实例化该类。

2、ImportSelector

  在@Configuration标注的Class上可以使用@Import引入其它的配置类,还可以引入org.springframework.context.annotation.ImportSelector实现类。ImportSelector接口只定义了一个selectImports(),用于指定需要注册为bean的Class名称。当在@Configuration标注的Class上使用@Import引入了一个ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。来看一个简单的示例,假设现在有一个接口HelloService,需要把所有它的实现类都定义为bean,而且它的实现类上是没有加Spring默认会扫描的注解的,比如@Component、@Service等。

public interface HelloService {
    void doSomething();
}

public class HelloServiceA implements HelloService {
    @Override
    public void doSomething() {
        System.out.println("Hello A");
    }
}

public class HelloServiceB implements HelloService {
    @Override
    public void doSomething() {
        System.out.println("Hello B");
    }
}

现定义了一个ImportSelector实现类HelloImportSelector,直接指定了需要把HelloService接口的实现类HelloServiceA和HelloServiceB定义为bean。

public class HelloImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {HelloServiceA.class.getName(), HelloServiceB.class.getName()};
    }
}

然后定义了@Configuration配置类HelloConfiguration,指定了@Import的是HelloImportSelector。

@Configuration
@Import(HelloImportSelector.class)
public class HelloConfiguration {

}

这样当加载配置类HelloConfiguration的时候会一并把HelloServiceA和HelloServiceB注册为Spring bean。可以进行如下简单测试:

@ContextConfiguration(classes=HelloConfiguration.class)
@RunWith(SpringRunner.class)
public class HelloImportSelectorTest {

    @Autowired
    private List<HelloService> helloServices;
    
    @Test
    public void test() {
        this.helloServices.forEach(HelloService::doSomething);
    }
    
}

看到这里可能你会觉得其实它也没什么用,因为整一个ImportSelector实现类那么麻烦,还不如直接在HelloConfiguration中定义bean或者import。在不引入ImportSelector的情况下,下面的两种方式都可以达到相同的效果。

@Configuration
@Import({HelloServiceA.class, HelloServiceB.class})
public class HelloConfiguration {
    
}
@Configuration
public class HelloConfiguration {

    @Bean
    public HelloServiceA helloServiceA() {
        return new HelloServiceA();
    }
    
    @Bean
    public HelloServiceB helloServiceB() {
        return new HelloServiceB();
    }
    
}

  如果直接是固定的bean定义,那完全可以用上面的方式代替,但如果需要动态的带有逻辑性的定义bean,则使用ImportSelector还是很有用处的。因为在它的selectImports()你可以实现各种获取bean Class的逻辑,通过其参数AnnotationMetadata importingClassMetadata可以获取到@Import标注的Class的各种信息,包括其Class名称,实现的接口名称、父类名称、添加的其它注解等信息,通过这些额外的信息可以辅助我们选择需要定义为Spring bean的Class名称。现假设我们在HelloConfiguration上使用了@ComponentScan进行bean定义扫描,我们期望HelloImportSelector也可以扫描@ComponentScan指定的Package下HelloService实现类并把它们定义为bean,则HelloImportSelector和HelloConfiguration可以改为如下这样:

@Configuration
@ComponentScan("com.elim.spring.core.importselector")
@Import(HelloImportSelector.class)
public class HelloConfiguration {

}

 

public class HelloImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getName());
        String[] basePackages = (String[]) annotationAttributes.get("basePackages");
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        TypeFilter helloServiceFilter = new AssignableTypeFilter(HelloService.class);
        scanner.addIncludeFilter(helloServiceFilter);
        Set<String> classes = new HashSet<>();
        for (String basePackage : basePackages) {
            scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> classes.add(beanDefinition.getBeanClassName()));
        }
        return classes.toArray(new String[classes.size()]);
    }

}

  可以看到在HelloImportSelector的实现中获取了HelloConfiguration类上标注的@ComponentScan的basePackages属性值,并使用ClassPathScanningCandidateComponentProvider进行了扫描。可能有的时候你不希望依赖于配置类上的@ComponentScan,而期望直接扫描配置类所在的包。此时可以通过importingClassMetadata.getClassName()获取配置类的Class名称,进而获取其package名称。

public class HelloImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        String packageName = null;
        try {
            packageName = Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        String[] basePackages = new String[] {packageName};
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        TypeFilter helloServiceFilter = new AssignableTypeFilter(HelloService.class);
        scanner.addIncludeFilter(helloServiceFilter);
        Set<String> classes = new HashSet<>();
        for (String basePackage : basePackages) {
            scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> classes.add(beanDefinition.getBeanClassName()));
        }
        return classes.toArray(new String[classes.size()]);
    }

}

  更通用一点的做法可能你还是期望扫描的package跟@Configuration上的@ComponentScan的basePackages保持一致或者在没有指定@ComponentScan时扫描配置类所在的package。@ComponentScan的basePackages如果没有指定,默认是把配置类当前所在的package当做basePackage。所以为了满足这些需求,我们的HelloImportSelector可以定义为如下这样:

public class HelloImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        String[] basePackages = null;
        if (importingClassMetadata.hasAnnotation(ComponentScan.class.getName())) {
            Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getName());
            basePackages = (String[]) annotationAttributes.get("basePackages");
        }
        if (basePackages == null || basePackages.length == 0) {//ComponentScan的basePackages默认为空数组
            String basePackage = null;
            try {
                basePackage = Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            basePackages = new String[] {basePackage};
        }
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        TypeFilter helloServiceFilter = new AssignableTypeFilter(HelloService.class);
        scanner.addIncludeFilter(helloServiceFilter);
        Set<String> classes = new HashSet<>();
        for (String basePackage : basePackages) {
            scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> classes.add(beanDefinition.getBeanClassName()));
        }
        return classes.toArray(new String[classes.size()]);
    }

}

3、为ImportSelector定义特定的注解

  当我们觉得在@Configuration配置类上使用@Import(HelloImportSelector.class)太麻烦,或者是需要在ImportSelector实现类中使用一些特定的配置时就可以考虑为ImportSelector实现类定义一个特定的注解,在该注解上使用@Import(HelloImportSelector.class)。如下针对上面的HelloImportSelector定义了一个@HelloServiceScan注解,用于扫描HelloService实现类。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(HelloImportSelector.class)
public @interface HelloServiceScan {

}

此时,我们的HelloConfiguration类可以改为如下这样,效果跟之前的一样的。

@Configuration
@HelloServiceScan
public class HelloConfiguration {

}

有了自定义注解后,就可以定义自定义注解的属性,以供在扫描bean时进行一些特殊的配置。比如可以把扫描的路径定义到自定义的注解中,而不必依赖于@ComponentScan。下面的代码中就为@HelloServiceScan自定义了属性basePackages和value,它俩互为别名。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(HelloImportSelector.class)
public @interface HelloServiceScan {

    @AliasFor("value")
    String[] basePackages() default {};
    
    @AliasFor("basePackages")
    String[] value() default {};
    
}

这样HelloImportSelector在进行bean扫描时可以通过@HelloServiceScan的basePackages属性获取需要扫描的basePackage。

public class HelloImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(HelloServiceScan.class.getName());
        String[] basePackages = (String[]) annotationAttributes.get("basePackages");
        if (basePackages == null || basePackages.length == 0) {//HelloServiceScan的basePackages默认为空数组
            String basePackage = null;
            try {
                basePackage = Class.forName(importingClassMetadata.getClassName()).getPackage().getName();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            basePackages = new String[] {basePackage};
        }
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        TypeFilter helloServiceFilter = new AssignableTypeFilter(HelloService.class);
        scanner.addIncludeFilter(helloServiceFilter);
        Set<String> classes = new HashSet<>();
        for (String basePackage : basePackages) {
            scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> classes.add(beanDefinition.getBeanClassName()));
        }
        return classes.toArray(new String[classes.size()]);
    }

}

@Configuration配置类上就可以为@HelloServiceScan指定额外的basePackages属性了。

@Configuration
@HelloServiceScan("com.elim.spring.core.importselector")
public class HelloConfiguration {

}

4、一个示例

在springboot项目中引入maven外部工程封装的一个工具类的jar。一般需要指定扫描的外部包将其需要引入的bean注入到当前容器。

@SpringBootApplication(scanBasePackages = {xxx包名})

现在应用上面介绍的注解实现一下:

在启动类上标注注解@EnableDelayQueueExtension

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Import({DelayQueueFeignRegistrar.class})
public @interface EnableDelayQueueExtension {
    boolean hasDelayQueue() default true;
}

 

public class DelayQueueFeignRegistrar implements ImportSelector {
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(EnableDelayQueueExtension.class.getName()));
        boolean hasDelayQueue = attributes.getBoolean("hasDelayQueue");
        List<String> list = new ArrayList();
        if (hasDelayQueue) {
            list.add(DelayQueueConfig.class.getName());
        }
        return (String[])list.toArray(new String[1]);
    }
}

 



参考文章:https://blog.csdn.net/elim168/article/details/88131614

posted @ 2022-03-18 10:26  字节悦动  阅读(731)  评论(0编辑  收藏  举报