本篇主要介绍Spring
注解@Import
的魅力所在:它能让你高度自由的定义配置类装载规则与Bean注册逻辑。@Import是Spring
体系中的一个比较重要的注解,下面让我们一起看看它都有哪些神奇的魅力吧!
注册Bean
@Import
第一个功能就是注册Bean
到Spring
容器中,用法非常简单,将需要注册到Spring
的类通过@Import
直接导入即可,这时候注册的Bean
对应的名称为类完全限定名。如下所示:
package com.swj.mj.autoconfig.service;
@Slf4j
public class HelloService {
public HelloService() {
log.info("HelloService Initialization...");
}
public String sayHello(String name) {
return "Hello " + name;
}
}
首先任意定义一个类,然后我们直接在某个Bean
或者配置类上通过@Import(HelloService.class)
方法注册Bean
,如下:
package com.swj.mj.web.hello;
@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@Import(HelloService.class)
public class HelloController implements ApplicationContextAware {
private static ApplicationContext appCtx;
private final HelloService helloService;
@GetMapping
public Map<String, Object> hello(String name) {
String qualifiedBeanName = HelloService.class.getName();
assert appCtx.containsBean(qualifiedBeanName) : "Spring容器中找不到名称为" + qualifiedBeanName + "的Bean";
String[] beanNames = appCtx.getBeanNamesForType(HelloService.class);
assert beanNames.length == 1 && qualifiedBeanName.equals(beanNames[0]);
return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appCtx = applicationContext;
}
}
创建一个启动类:
package com.swj.mj.web;
@SpringBootApplication
public class WebApplication {
public static void main(String[] args) throws Exception {
SpringApplication.run(WebApplication.class, args);
}
}
添加JVM
选项-ea
(激活断言功能)并启动容器后(启动类的所在包路径为com.swj.mj.web
,并且没有额外配置扫描包,所以默认扫描的包是com.swj.mj.web
),可以看到如下日志打印,说明@Import
可以用来注册Bean
。

同时,请求后可以看到两个断言都成立,说明通过@Import
注册的SpringBean
的beanName
为类对应的类完全限定名。

如果我们去掉了@Import(HelloService.class)
,那么应用启动后将直接失败并终止:

导入配置类
第二个功能是直接导入配置类。我们在原先的基础上新增一个配置类HelloServiceConfiguration
:
@Configuration
public class HelloServiceConfiguration {
@Bean
@ConditionalOnMissingBean
public HelloService helloService() {
return new HelloService();
}
}
该配置类会在容器中没有helloService
这个Bean
时自动注册HelloService
。然后修改HelloController
:
@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@Import(HelloServiceConfiguration.class)
public class HelloController implements ApplicationContextAware {
private static ApplicationContext appCtx;
private final HelloService helloService;
@GetMapping
public Map<String, Object> hello(String name) {
String beanName = "helloService";
assert appCtx.containsBean(beanName) : "Spring容器中找不到名称为" + beanName + "的Bean";
String[] beanNames = appCtx.getBeanNamesForType(HelloService.class);
assert beanNames.length == 1 && beanName.equals(beanNames[0]);
return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
appCtx = applicationContext;
}
}
这里通过@Import(HelloServiceConfiguration.class)
直接导入配置类,由配置类注册HelloService
,这时候的beanName
就不是类完全限定名了,而是方法名helloService
。
实际上HelloServiceConfiguration
同样会被注册到Spring
容器中,通过appCtx.getBean(HelloServiceConfiguration.class)
可以得到配置类。所以可以说第一个功能与第二个功能是重合的,这点在源码上也可以得到解释,具体可以参考org.springframework.context.annotation.ConfigurationClassParser#processImports
。本篇不讲源码哈,只介绍如何使用。
选择性装载配置类
第三个功能是通过实现org.springframework.context.annotation.ImportSelector
接口完成动态开启配置类。
首先看一下ImportSelector
接口的定义:
public interface ImportSelector {
/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
* @return the class names, or an empty array if none
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
/**
* Return a predicate for excluding classes from the import candidates, to be
* transitively applied to all classes found through this selector's imports.
* <p>If this predicate returns {@code true} for a given fully-qualified
* class name, said class will not be considered as an imported configuration
* class, bypassing class file loading as well as metadata introspection.
* @return the filter predicate for fully-qualified candidate class names
* of transitively imported configuration classes, or {@code null} if none
* @since 5.2.4
*/
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
主要关注String[] selectImports(AnnotationMetadata importingClassMetadata)
方法,参数importingClassMetadata
表示使用了@Import(? extends ImportSelector)
注解的类的元数据。通过该参数可以实现获取一些组合注解的自定义属性等内容,从而实现选择性装载配置类。
「注意这个方法的返回值不能是null
,极端情况请返回空数组,否则运行将抛出NPE
。」
让我们先创建一个SpringUtil
:
package com.swj.mj.autoconfig.util;
public class SpringUtil implements BeanFactoryAware, ApplicationContextAware, Ordered {
private static ApplicationContext appCtx;
private static BeanFactory beanFactory;
public static ApplicationContext getAppCtx() {
return appCtx;
}
public static BeanFactory getBeanFactory() {
return beanFactory;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
SpringUtil.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.appCtx = applicationContext;
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 1;
}
}
这个类主要用于通过静态持有Spring
的ApplicationContext
和BeanFactory
,后续可以直接通过该类访问Spring
容器。然后创建我们的ImportSelector
配置类装载选择器:
package com.swj.mj.autoconfig.configuration;
public class CustomImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// importingClassMetadata 指使用 @Import(HelloServiceImportSelector) 时 @Import 注解所在类的元数据
String enableCustomConfig = EnableCustomConfig.class.getName();
if (importingClassMetadata.hasAnnotation(enableCustomConfig)) {
Map<String, Object> attrs = importingClassMetadata.getAnnotationAttributes(enableCustomConfig);
if (MapUtils.isNotEmpty(attrs)) {
String registerUtil = Optional.ofNullable(attrs.get("registerUtil")).map(Object::toString).orElse("false");
if (Boolean.parseBoolean(registerUtil)) {
return new String[]{
HelloServiceConfiguration.class.getName(),
SpringUtil.class.getName()
};
}
}
}
return new String[]{
HelloServiceConfiguration.class.getName()
};
}
}
然后创建我们的EnableCustomConfig
注解:
package com.swj.mj.autoconfig.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomImportSelector.class)
public @interface EnableCustomConfig {
/**
* 是否注册 {@link com.swj.mj.autoconfig.util.SpringUtil} 工具类,默认为 {@literal true}
*/
boolean registerUtil() default true;
}
这里简单解释一下:首先@EnableCustomConfig
注解组合了@Import(CustomImportSelector.class)
,这样在CustomImportSelector#selectImports
方法中可以通过importingClassMetadata
获取得到@EnableCustomConfig
注解的属性配置。然后通过该配置实现添加装载配置。这里我们判断registerUtil
是否为true
(默认为true
),为true
时将装载SpringUtil
以便后续可以通过SpringUtil
访问Spring
容器。
接着调整HelloController
:
@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@EnableCustomConfig
public class HelloController {
private final HelloService helloService;
@GetMapping
public Map<String, Object> hello(String name) {
ApplicationContext appCtx = SpringUtil.getAppCtx();
assert Objects.nonNull(appCtx) : "appCtx 为 null";
assert appCtx.containsBean(SpringUtil.class.getName());
assert appCtx.containsBean(HelloServiceConfiguration.class.getName());
assert appCtx.getBean(HelloService.class) == helloService;
return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
}
}
这里通过添加@EnableCustomConfig
激活联动激活@Import(CustomImportSelector.class)
以注册SpringUtil
和HelloServiceConfiguration
,由于默认registerUtil
为true
,所以会注册SpringUtil
。启动应用后能访问http://localhost:8080/hello?name=Reka
表示断言均成立,容器中确实注册了对应的Bean
。
接着我们调整@EnableCustomConfig
为@EnableCustomConfig(registerUtil = false)
,再次启动容器访问http://localhost:8080/hello?name=Reka
,这时候会发现断言失败:

动态注册Bean
这是@Import
的第四个功能,通过org.springframework.context.annotation.ImportBeanDefinitionRegistrar
自由注册SpringBean
。该接口的定义如下:
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
主要看void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)
,其中参数importingClassMetadata
与ImportSelector
里的参数效用一致,registry
参数简单理解是Bean
注册器,通过它可以往Spring
容器中注册Bean
。现在让我们通过动态注册Bean
的方式实现CustomImportSelector
的功能。
package com.swj.mj.autoconfig.configuration;
public class CustomImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
AnnotationAttributes attrs = AnnotatedElementUtils.getMergedAnnotationAttributes(
ClassUtils.resolveClassName(importingClassMetadata.getClassName(), null),
EnableCustomBean.class);
if (MapUtils.isNotEmpty(attrs) && BooleanUtils.isTrue(attrs.getBoolean("registerUtil"))) {
registry.registerBeanDefinition("springUtil", new RootBeanDefinition(SpringUtil.class));
}
registry.registerBeanDefinition("helloService", new RootBeanDefinition(HelloService.class));
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CustomImportBeanDefinitionRegistrar.class)
public @interface EnableCustomBean {
/**
* 是否注册 {@link com.swj.mj.autoconfig.util.SpringUtil} 工具类,默认为 {@literal true}
*/
boolean registerUtil() default true;
}
CustomImportBeanDefinitionRegistrar
的逻辑与CustomImportSelector
基本一致,只不过最后不是返回配置类,而是直接通过org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition
注册Bean
。最后调整一下HelloController
:
@RestController
@AllArgsConstructor
@RequestMapping("/hello")
@EnableCustomBean
public class HelloController {
private final HelloService helloService;
@GetMapping
public Map<String, Object> hello(String name) {
ApplicationContext appCtx = SpringUtil.getAppCtx();
assert Objects.nonNull(appCtx) : "appCtx 为 null";
assert appCtx.containsBean("springUtil");
assert appCtx.getBean(HelloService.class) == helloService;
return ImmutableMap.of("name", name, "helloResult", helloService.sayHello(name));
}
}
实际运行效果与CustomImportSelect
和@EnableCustomConfig
一致,请读者自行验证。
如果仅仅是这样,你是不是认为就没必要有ImportBeanDefinitionRegistrar
了。实际上通过该接口我们可以定义自己的容器Bean
注解。实现很多特殊的功能:比如在三维家美家技术中使用到了SpringCloud Stream
,但SCS
的@org.springframework.cloud.stream.annotation.EnableBinding
注解很麻烦,每次引入新的@Input
和@Output
都要将对应接口添加到@EnableBinding
中,三维家美家通过ImportBeanDefinitionRegistrar
和自定义注解实现新的Input/Ouput Bean
扫描注册流程。后续会计划将该功能提PR
到SpringCloud
项目中。
通过自定义注册和ImportBeanDefinitionRegistrar
可以更灵活地自定义Bean
注册逻辑,限于篇幅原因,我们往后有机会再讲。各位,今天关于@Import
的基本用法是否掌握了呢,建议可以自己实践一次以加深理解,如果对源码有兴趣,可以阅读org.springframework.context.annotation.ConfigurationClassParser
类。
原文始发于微信公众号(三维家技术实践):@Import注解的魅力
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/30576.html