使用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容器中的。
今天我们将借助于@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进行探讨。
评论系统未开启,无法评论!