Spring Boot中@Import三种使用方式实例详解
作者:雨点的名字
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类似这种的时候,用它处理更为方便(因为扫描到了直接注册即可)
到此这篇关于Spring Boot中@Import三种使用方式!的文章就介绍到这了,更多相关Spring Boot @Import使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!