0
点赞
收藏
分享

微信扫一扫

Spring注解@Import实现多模块中Bean的导入


1. 前言

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


Spring注解@Import实现多模块中Bean的导入_Import

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​等场景中。多多关注:​码农小胖哥​ 获取更多干货。

Spring注解@Import实现多模块中Bean的导入_ImportSelector_02


举报

相关推荐

0 条评论