Spring Boot中@Import三种使用方式!

需要注意的是:ImportSelector、ImportBeanDefinitionRegistrar这两个接口都必须依赖于@Import一起使用,而@Import可以单独使用。
@Import是一个非常有用的注解,它的长处在于你可以通过配置来控制是否注入该Bean,也可以通过条件来控制注入哪些Bean到Spring容器中。
比如我们熟悉的:@EnableAsync 、@EnableCaching、@EnableScheduling等等统一采用的都是借助@Import注解来实现的。
下面我们就通过示例来了解@Import三种用法!
一、引入普通类
有个用户类如下
@Data
public class UserConfig {
/** 用户名*/
private String username;
/**手机号*/
private String phone;
}
那么如何通过@Import注入容器呢?
@Import(UserConfig.class)
@Configuration
public class UserConfiguration {
}
当在@Configuration标注的类上使用@Import引入了一个类后,就会把该类注入容器中。
当然除了@Configuration 比如@Component、@Service等一样也可以。
测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserConfig userConfig;
@Test
public void getUser() {
String name = userConfig.getClass().getName();
System.out.println("name = " + name);
}
}
控制台输出
name = com.jincou.importselector.model.UserConfig
如果@Import的功能仅仅是这样,那其实它并没什么特别的价值,我们可以通过其它方式实现?
@Configuration
public class UserConfiguration {
@Bean
public UserConfig userConfig() {
return new UserConfig();
}
}
再比如直接添加@Configuration注解
@Configuration
public class UserConfig {
// ......
}
确实如果注入静态的Bean到容器中,那完全可以用上面的方式代替,但如果需要动态的带有逻辑性的注入Bean,那才更能体现@Import的价值。
二、引入ImportSelector的实现类
说到ImportSelector这个接口就不得不说这里面最重要的一个方法:selectImports()。
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
这个方法的返回值是一个字符串数组,只要在配置类被引用了,这里返回的字符串数组中的类名就会被Spring容器new出来,然后再把这些对象注入IOC容器中。
所以这有啥用呢?我们还是用一个例子演示一下。
1、静态import场景(注入已知的类)
我们先将上面的示例改造下:
自定义MyImportSelector实现ImportSelector接口,重写selectImports方法
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//这里目的是将UserConfig 注入容器中
return new String[]{"com.jincou.importselector.model.UserConfig"};
}
}
然后在配置类引用
@Import(MyImportSelector.class)
@Configuration
public class UserConfiguration {
}
这样一来同样可以通过成功将UserConfig注入容器中。
如果看到这,你肯定会有疑问。我这又是新建MyImportSelector类,又是实现ImportSelector重写selectImports方法,然后我这么做有个卵用呢?
直接把类上加个@Component注入进去不香吗?这个ImportSelector把简单的功能搞这么复杂。
接下来就要说说如何动态注入Bean了。
2、动态import场景(注入指定条件的类)
我们来思考一种场景,就是你想通过开关来控制是否注入该Bean,或者说通过配置来控制注入哪些Bean,这个时候就有了ImportSelector的用武之地了。
我们来举个例子,通过ImportSelector的使用实现条件选择是注入本地缓存还是Redis缓存。
1)、定义缓存接口和实现类
顶层接口
public interface CacheService {
void setData(String key);
}
本地缓存 实现类
public class LocalServicempl implements CacheService {
@Override
public void setData(String key) {
System.out.println("本地存储存储数据成功 key= " + key);
}
}
redis缓存实现类
public class RedisServicempl implements CacheService {
@Override
public void setData(String key) {
System.out.println("redis存储数据成功 key= " + key);
}
}
2)、定义ImportSelector实现类
以下代码中根据EnableMyCache注解中的不同值来切换缓存的实现类再spring中的注册。
public class MyCacheSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMyCache.class.getName());
//通过 不同type注入不同的缓存到容器中
CacheType type = (CacheType) annotationAttributes.get("type");
switch (type) {
case LOCAL: {
return new String[]{LocalServicempl.class.getName()};
}
case REDIS: {
return new String[]{RedisServicempl.class.getName()};
}
default: {
throw new RuntimeException(MessageFormat.format("unsupport cache type {0}", type.toString()));
}
}
}
}
3)、定义注解
@EnableMyCache注解就像一个开关,通过这个开关来是否将特定的Bean注入容器。
定义一个枚举
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyCacheSelector.class)
public @interface EnableMyCache {
CacheType type() default CacheType.REDIS;
}
public enum CacheType {
LOCAL, REDIS;
}
4)、测试
这里选择本地缓存。
@EnableMyCache(type = CacheType.LOCAL)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private CacheService cacheService;
@Test
public void test() {
cacheService.setData("key");
}
}
控制台输出
本地存储存储数据成功 key= key
切换成redis缓存
@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private CacheService cacheService;
@Test
public void test() {
cacheService.setData("key");
}
}
控制台输出
redis存储数据成功 key= key
这个示例不是就是Bean的动态注入了吗?
3、Spring如何使用ImportSelector的场景
SpringBoot有两个常用注解 @EnableAsync @EnableCaching 其实就是通过ImportSelector来动态注入Bean
看下@EnableAsync注解,它有通过@Import({AsyncConfigurationSelector.class})
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
Class<? extends Annotation> annotation() default Annotation.class;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
AsyncConfigurationSelector.class
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";
public AsyncConfigurationSelector() {
}
@Nullable
public String[] selectImports(AdviceMode adviceMode) {
switch(adviceMode) {
case PROXY:
return new String[]{ProxyAsyncConfiguration.class.getName()};
case ASPECTJ:
return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
default:
return null;
}
}
}
是不是和我上面写的示例一样。
总之,向这种还不能决定去注入哪个处理器(如果你能决定,那就直接@Import那个类好了,没必要实现接口了),就可以实现此接口,写出一些判断逻辑,不同的配置情况注入不同的处理类。
三、引入ImportBeanDefinitionRegister的实现类
当配置类实现了 ImportBeanDefinitionRegistrar 接口,你就可以自定义往容器中注册想注入的Bean。
这个接口相比与 ImportSelector 接口的主要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加属性之类的。
public class MyImportBean implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类,其registerBeanDefinition()可以注册bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
1、举一个简单的示例
我们通过先通过一个简单的小示例,来理解它的基本使用
假设有个用户配置类如下
@Data
public class UserConfig {
/** 用户名*/
private String username;
/**手机号*/
private String phone;
}
我们通过实现ImportBeanDefinitionRegistrar的方式来完成注入。
public class MyImportBean implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类,其registerBeanDefinition()可以注册bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//构建一个 BeanDefinition , Bean的类型为 UserConfig,这个Bean的属性username的值为后端元宇宙
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserConfig.class)
.addPropertyValue("username", "后端元宇宙")
.getBeanDefinition();
//把 UserConfig 这个Bean的定义注册到容器中
registry.registerBeanDefinition("userConfig", beanDefinition);
}
}
通过配置类 中引入MyImportBean对象。
@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {
}
我们再来测试下
@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserConfig userConfig;
@Test
public void test() {
String username = userConfig.getUsername();
System.out.println("username = " + username);
}
}
控制台输出
username = 后端元宇宙
说明通过ImportBeanDefinitionRegistrar方式,已经把UserConfig注入容器成功,而且还为给bean设置了新属性。
然后我们再来思考一个问题,就比如我们在其它地方已经将UserConfig注入容器,这里会不会出现冲突,或者不冲突的情况下,属性能不能设置成功?
我们来试下
@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {
/**
* 这里通过@Bean注解,将UserConfig注入Spring容器中,而且名称也叫userConfig
*/
@Bean
public UserConfig userConfig() {
return new UserConfig();
}
}
然后我们再来跑下上面的测试用例,发现报错了。

2、举一个复杂点的例子
Mybatis的@MapperScan就是用这种方式实现的,@MapperScan注解,指定basePackages,扫描Mybatis Mapper接口类注入到容器中。
这里我们自定义一个注解@MyMapperScan来扫描包路径下所以带@MapperBean注解的类,并将它们注入到IOC容器中。
1)、先定义一个@MapperBean注解,就相当于我们的@Mapper注解
/**
* 定义包路径。(指定包下所有添加了MapperBean注解的类作为bean)
* 注意这里 @Import(MyMapperScanImportBean.class) 的使用
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MapperBean {
}
2)、一个需要注入的bean,这里加上@MapperBean注解。
package com.jincou.importselector.mapperScan;
import com.jincou.importselector.config.MapperBean;
@MapperBean
public class User {
}
3)、再定一个扫描包路径的注解@MyMapperScan 就相当于mybatis的@MapperScan注解。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MyMapperScanImportBean.class)
public @interface MyMapperScan {
/**
* 扫描包路径
*/
String[] basePackages() default {};
}
4)、MyMapperScanImportBean实现ImportBeanDefinitionRegistrar接口
public class MyMapperScanImportBean implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private final static String PACKAGE_NAME_KEY = "basePackages";
private ResourceLoader resourceLoader;
/**
* 搜索指定包下所有添加了MapperBean注解的类,并且把这些类添加到ioc容器里面去
*
* @param importingClassMetadata 当前类的注解信息
* @param registry 注册类,其registerBeanDefinition()可以注册bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//1. 从BeanIocScan注解获取到我们要搜索的包路径
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyMapperScan.class.getName()));
if (annoAttrs == null || annoAttrs.isEmpty()) {
return;
}
String[] basePackages = (String[]) annoAttrs.get(PACKAGE_NAME_KEY);
// 2. 找到指定包路径下所有添加了MapperBean注解的类,并且把这些类添加到IOC容器里面去
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
scanner.setResourceLoader(resourceLoader);
//路径包含MapperBean的注解的bean
scanner.addIncludeFilter(new AnnotationTypeFilter(MapperBean.class));
//扫描包下路径
scanner.scan(basePackages);
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
5)测试
这里扫描的路径就是上面User实体的位置
@RunWith(SpringRunner.class)
@SpringBootTest
@MyMapperScan(basePackages = {"com.jincou.importselector.mapperScan"})
public class UserServiceTest {
@Autowired
private User user;
@Test
public void test() {
System.out.println("username = " + user.getClass().getName());
}
}
运行结果
username = com.jincou.importselector.mapperScan.User
完美,成功!
实现它的基本思想是:当自己需要操作BeanFactory里面的Bean的时候,那就必须只有它才能做到了。而且它还有个方便的地方,那就是做包扫描的时候,比如@MapperScan类似这种的时候,用它处理更为方便(因为扫描到了直接注册即可)
声明: 公众号如需转载该篇文章,发表文章的头部一定要 告知是转至公众号: 后端元宇宙。同时也可以问本人要markdown原稿和原图片。其它情况一律禁止转载!
Spring Boot中@Import三种使用方式!的更多相关文章
- python中的三种输入方式
python中的三种输入方式 python2.X python2.x中以下三个函数都支持: raw_input() input() sys.stdin.readline() raw_input( )将 ...
- Asp.Net中的三种分页方式
Asp.Net中的三种分页方式 通常分页有3种方法,分别是asp.net自带的数据显示空间如GridView等自带的分页,第三方分页控件如aspnetpager,存储过程分页等. 第一种:使用Grid ...
- SQL Server中的三种Join方式
1.测试数据准备 参考:Sql Server中的表访问方式Table Scan, Index Scan, Index Seek 这篇博客中的实验数据准备.这两篇博客使用了相同的实验数据. 2.SQ ...
- Spring Boot2 系列教程(二)创建 Spring Boot 项目的三种方式
我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 43W+,如下图: 2017 年由于种种原因,就没有 ...
- Java之线程安全中的三种同步方式
一个程序在运行起来时,会转换为进程,通常含有多个线程. 通常情况下,一个进程中的比较耗时的操作(如长循环.文件上传下载.网络资源获取等),往往会采用多线程来解决. 比如,现实生活中,银行取钱问题.火车 ...
- C++中的三种继承方式
1,被忽略的细节: 1,冒号( :)表示继承关系,Parent 表示被继承的类,public 的意义是什么? class Parent { }; class Child : public Parent ...
- 关于selenium中的三种等待方式与EC模块的知识
1. 强制等待 第一种也是最简单粗暴的一种办法就是强制等待sleep(xx),强制让闪电侠等xx时间,不管凹凸曼能不能跟上速度,还是已经提前到了,都必须等xx时间. 看代码: 1 2 3 4 5 6 ...
- selenium中的三种等待方式(显示等待WebDriverWait()、隐式等待implicitly()、强制等待sleep())---基于python
我们在实际使用selenium或者appium时,等待下个等待定位的元素出现,特别是web端加载的过程,都需要用到等待,而等待方式的设置是保证脚本稳定有效运行的一个非常重要的手段,在selenium中 ...
- selenium&appium中的三种等待方式---基于python
我们在实际使用selenium或者appium时,等待下个等待定位的元素出现,特别是web端加载的过程,都需要用到等待,而等待方式的设置是保证脚本稳定有效运行的一个非常重要的手段,在selenium中 ...
- Spring——自动装配的三种实现方式
依赖注入的本质是装配,装配是依赖注入的具体行为 spring会在上下文中自动寻找,并自动给bean装配属性 自动装配的三种方式 (1).在xml中显式的装配 (2).在java中显式的装配 (3).隐 ...
随机推荐
- CDH6.2.0安装并使用基于HBase的Geomesa
1. 查看CDH 安装的hadoop 和 hbase 对应的版本 具体可以参考以下博客: https://www.cxyzjd.com/article/spark_Streaming/10876290 ...
- 【ASP.NET Core】自定义Session的存储方式
在开始今天的表演之前,老周先跟大伙伴们说一句:"中秋节快乐". 今天咱们来聊一下如何自己动手,实现会话(Session)的存储方式.默认是存放在分布式内存中.由于HTTP消息是无状 ...
- LibTorch 多项分布
最近在学习过程中需要对服从某种分布的离散型随机变量进行抽样,在LibTroch中查到了torch::multinomial(多项分布),该方法的接口如下: at::Tensor multinomial ...
- 【问题解决】Debian更新源提示InRelease已过期
问题 本人日常用 Debian10 今天在更新源(apt update) 时,出现InRelease文件过期的问题 E: http://mirrors.163.com/debian/dists/bus ...
- Elastic:应用程序性能监控/管理(APM)实践
在今天的文章里,我们将介绍Elastic的一个重要的应用:应用程序性能管理(Application Performance Monitoring/Management),简称APM.那么到底什么是AP ...
- Ceph 存储集群第一部分:配置和部署
内容来源于官方,经过个人实践操作整理,官方地址:http://docs.ceph.org.cn/rados/ 所有 Ceph 部署都始于 Ceph 存储集群. 基于 RADOS 的 Ceph 对象存储 ...
- 第四章:Django表单 - 2:Django表单API详解
声明:以下的Form.表单等术语都指的的广义的Django表单. Form要么是绑定了数据的,要么是未绑定数据的. 如果是绑定的,那么它能够验证数据,并渲染表单及其数据,然后生成HTML表单.如果未绑 ...
- NSIS自定义目录选择页面制作之安装…
在nsis制作自定义界面中,目录选择页面个人感觉最为繁琐,因为该界面不仅涉及到界面控件的创建,还要涉及到控件消息传递和状态改变时的回调函数通告. 迅雷界面为例: 其中安装目录中的8盘符,在本机中并不存 ...
- centos7中配置java + mysql +jdk +使用jar部署项目
centos7中配置java + mysql +jdk +使用jar部署项目 思维导图 1. 配置JDK環境 1.1下载jdk安装包 Java Downloads | Oracle 1.2 将下载j ...
- TF-GNN踩坑记录(三)
引言 Batch size问题 在Tensorflow-GNN中使用batch size除了需要注意上面的链接问题之外,最近我在调试的发现,使用了merge_batch_to_components() ...