转载于:公众号:后端元宇宙 需要注意的是:ImportSelector、ImportBeanDefinitionRegistrar这两个接口都必须依赖于Im port一起使用,而Import可以单独使用。 Import是一个非常有用的注解,它的长处在于你可以通过配置来控制是否注入该Bean,也可以通过条件来控制注入哪些Bean到Spring容器中。 比如我们熟悉的:EnableAsync、EnableCaching、EnableScheduling等等统一采用的都是借助Import注解来实现的。 下面我们就通过示例来了解Import三种用法!一、引入普通类 有个用户类如下DatapublicclassUserConfig{用户名privateStringusername;手机号privateStringphone;} 那么如何通过Import注入容器呢?Import(UserConfig。class)ConfigurationpublicclassUserConfiguration{} 当在Configuration标注的类上使用Import引入了一个类后,就会把该类注入容器中。 当然除了Configuration比如Component、Service等一样也可以。 测试SpringBootTestRunWith(SpringRunner。class)publicclassUserServiceTest{AutowiredprivateUserConfiguserConfig;TestpublicvoidgetUser(){StringnameuserConfig。getClass()。getName();System。out。println(namename);}} 控制台输出namecom。jincou。importselector。model。UserConfig 如果Import的功能仅仅是这样,那其实它并没什么特别的价值,我们可以通过其它方式实现?ConfigurationpublicclassUserConfiguration{BeanpublicUserConfiguserConfig(){returnnewUserConfig();}} 再比如直接添加Configuration注解ConfigurationpublicclassUserConfig{。。。。。。} 确实如果注入静态的Bean到容器中,那完全可以用上面的方式代替,但如果需要动态的带有逻辑性的注入Bean,那才更能体现Import的价值。 二、引入ImportSelector的实现类 说到ImportSelector这个接口就不得不说这里面最重要的一个方法:selectImports()。publicinterfaceImportSelector{String〔〕selectImports(AnnotationMetadataimportingClassMetadata);} 这个方法的返回值是一个字符串数组,只要在配置类被引用了,这里返回的字符串数组中的类名就会被Spring容器new出来,然后再把这些对象注入IOC容器中。 所以这有啥用呢?我们还是用一个例子演示一下。1、静态import场景(注入已知的类) 我们先将上面的示例改造下: 自定义MyImportSelector实现ImportSelector接口,重写selectImports方法publicclassMyImportSelectorimplementsImportSelector{OverridepublicString〔〕selectImports(AnnotationMetadataimportingClassMetadata){这里目的是将UserConfig注入容器中returnnewString〔〕{com。jincou。importselector。model。UserConfig};}} 然后在配置类引用Import(MyImportSelector。class)ConfigurationpublicclassUserConfiguration{} 这样一来同样可以通过成功将UserConfig注入容器中。 如果看到这,你肯定会有疑问。我这又是新建MyImportSelector类,又是实现ImportSelector重写selectImports方法,然后我这么做有个卵用呢? 直接把类上加个Component注入进去不香吗?这个ImportSelector把简单的功能搞这么复杂。 接下来就要说说如何动态注入Bean了。2、动态import场景(注入指定条件的类) 我们来思考一种场景,就是你想通过开关来控制是否注入该Bean,或者说通过配置来控制注入哪些Bean,这个时候就有了ImportSelector的用武之地了。 我们来举个例子,通过ImportSelector的使用实现条件选择是注入本地缓存还是Redis缓存。 1)、定义缓存接口和实现类 顶层接口publicinterfaceCacheService{voidsetData(Stringkey);} 本地缓存实现类publicclassLocalServicemplimplementsCacheService{OverridepublicvoidsetData(Stringkey){System。out。println(本地存储存储数据成功keykey);}} redis缓存实现类publicclassRedisServicemplimplementsCacheService{OverridepublicvoidsetData(Stringkey){System。out。println(redis存储数据成功keykey);}} 2)、定义ImportSelector实现类 以下代码中根据EnableMyCache注解中的不同值来切换缓存的实现类再spring中的注册。publicclassMyCacheSelectorimplementsImportSelector{OverridepublicString〔〕selectImports(AnnotationMetadataimportingClassMetadata){MapString,ObjectannotationAttributesimportingClassMetadata。getAnnotationAttributes(EnableMyCache。class。getName());通过不同type注入不同的缓存到容器中CacheTypetype(CacheType)annotationAttributes。get(type);switch(type){caseLOCAL:{returnnewString〔〕{LocalServicempl。class。getName()};}caseREDIS:{returnnewString〔〕{RedisServicempl。class。getName()};}default:{thrownewRuntimeException(MessageFormat。format(unsupportcachetype{0},type。toString()));}}}} 3)、定义注解 EnableMyCache注解就像一个开关,通过这个开关来是否将特定的Bean注入容器。 定义一个枚举Target(ElementType。TYPE)Retention(RetentionPolicy。RUNTIME)DocumentedImport(MyCacheSelector。class)publicinterfaceEnableMyCache{CacheTypetype()defaultCacheType。REDIS;}publicenumCacheType{LOCAL,REDIS;} 4)、测试 这里选择本地缓存。EnableMyCache(typeCacheType。LOCAL)SpringBootTestRunWith(SpringRunner。class)publicclassUserServiceTest{AutowiredprivateCacheServicecacheService;Testpublicvoidtest(){cacheService。setData(key);}} 控制台输出本地存储存储数据成功keykey 切换成redis缓存EnableMyCache(typeCacheType。REDIS)SpringBootTestRunWith(SpringRunner。class)publicclassUserServiceTest{AutowiredprivateCacheServicecacheService;Testpublicvoidtest(){cacheService。setData(key);}} 控制台输出redis存储数据成功keykey 这个示例不是就是Bean的动态注入了吗?3、Spring如何使用ImportSelector的场景 SpringBoot有两个常用注解EnableAsyncEnableCaching其实就是通过ImportSelector来动态注入Bean 看下EnableAsync注解,它有通过Import({AsyncConfigurationSelector。class})Target({ElementType。TYPE})Retention(RetentionPolicy。RUNTIME)DocumentedImport({AsyncConfigurationSelector。class})publicinterfaceEnableAsync{Classlt;?extendsAnnotationannotation()defaultAnnotation。class;booleanproxyTargetClass()defaultfalse;AdviceModemode()defaultAdviceMode。PROXY;intorder()default2147483647;} AsyncConfigurationSelector。classpublicclassAsyncConfigurationSelectorextendsAdviceModeImportSelectorEnableAsync{privatestaticfinalStringASYNCEXECUTIONASPECTCONFIGURATIONCLASSNAMEorg。springframework。scheduling。aspectj。AspectJAsyncConfiguration;publicAsyncConfigurationSelector(){}NullablepublicString〔〕selectImports(AdviceModeadviceMode){switch(adviceMode){casePROXY:returnnewString〔〕{ProxyAsyncConfiguration。class。getName()};caseASPECTJ:returnnewString〔〕{org。springframework。scheduling。aspectj。AspectJAsyncConfiguration};default:returnnull;}}} 是不是和我上面写的示例一样。 总之,向这种还不能决定去注入哪个处理器(如果你能决定,那就直接Import那个类好了,没必要实现接口了),就可以实现此接口,写出一些判断逻辑,不同的配置情况注入不同的处理类。 三、引入ImportBeanDefinitionRegister的实现类 当配置类实现了ImportBeanDefinitionRegistrar接口,你就可以自定义往容器中注册想注入的Bean。 这个接口相比与ImportSelector接口的主要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,但是ImportBeanDefinitionRegistrar是可以自己注入BeanDefinition,可以添加属性之类的。publicclassMyImportBeanimplementsImportBeanDefinitionRegistrar{paramimportingClassMetadata当前类的注解信息paramregistry注册类,其registerBeanDefinition()可以注册beanOverridepublicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){}}1、举一个简单的示例 我们通过先通过一个简单的小示例,来理解它的基本使用 假设有个用户配置类如下DatapublicclassUserConfig{用户名privateStringusername;手机号privateStringphone;} 我们通过实现ImportBeanDefinitionRegistrar的方式来完成注入。publicclassMyImportBeanimplementsImportBeanDefinitionRegistrar{paramimportingClassMetadata当前类的注解信息paramregistry注册类,其registerBeanDefinition()可以注册beanOverridepublicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){构建一个BeanDefinition,Bean的类型为UserConfig,这个Bean的属性username的值为后端元宇宙AbstractBeanDefinitionbeanDefinitionBeanDefinitionBuilder。rootBeanDefinition(UserConfig。class)。addPropertyValue(username,后端元宇宙)。getBeanDefinition();把UserConfig这个Bean的定义注册到容器中registry。registerBeanDefinition(userConfig,beanDefinition);}} 通过配置类中引入MyImportBean对象。Import(MyImportBean。class)ConfigurationpublicclassUserImportConfiguration{} 我们再来测试下EnableMyCache(typeCacheType。REDIS)SpringBootTestRunWith(SpringRunner。class)publicclassUserServiceTest{AutowiredprivateUserConfiguserConfig;Testpublicvoidtest(){StringusernameuserConfig。getUsername();System。out。println(usernameusername);}} 控制台输出username后端元宇宙 说明通过ImportBeanDefinitionRegistrar方式,已经把UserConfig注入容器成功,而且还为给bean设置了新属性。 然后我们再来思考一个问题,就比如我们在其它地方已经将UserConfig注入容器,这里会不会出现冲突,或者不冲突的情况下,属性能不能设置成功? 我们来试下Import(MyImportBean。class)ConfigurationpublicclassUserImportConfiguration{这里通过Bean注解,将UserConfig注入Spring容器中,而且名称也叫userConfigBeanpublicUserConfiguserConfig(){returnnewUserConfig();}} 然后我们再来跑下上面的测试用例,发现报错了。 2、举一个复杂点的例子 Mybatis的MapperScan就是用这种方式实现的,MapperScan注解,指定basePackages,扫描MybatisMapper接口类注入到容器中。 这里我们自定义一个注解MyMapperScan来扫描包路径下所以带MapperBean注解的类,并将它们注入到IOC容器中。 1)、先定义一个MapperBean注解,就相当于我们的Mapper注解定义包路径。(指定包下所有添加了MapperBean注解的类作为bean)注意这里Import(MyMapperScanImportBean。class)的使用Retention(RetentionPolicy。RUNTIME)Target(ElementType。TYPE)DocumentedpublicinterfaceMapperBean{} 2)、一个需要注入的bean,这里加上MapperBean注解。packagecom。jincou。importselector。mapperScan;importcom。jincou。importselector。config。MapperBean;MapperBeanpublicclassUser{} 3)、再定一个扫描包路径的注解MyMapperScan就相当于mybatis的MapperScan注解。Retention(RetentionPolicy。RUNTIME)Target(ElementType。TYPE)DocumentedImport(MyMapperScanImportBean。class)publicinterfaceMyMapperScan{扫描包路径String〔〕basePackages()default{};} 4)、MyMapperScanImportBean实现ImportBeanDefinitionRegistrar接口publicclassMyMapperScanImportBeanimplementsImportBeanDefinitionRegistrar,ResourceLoaderAware{privatefinalstaticStringPACKAGENAMEKEYbasePackages;privateResourceLoaderresourceLoader;搜索指定包下所有添加了MapperBean注解的类,并且把这些类添加到ioc容器里面去paramimportingClassMetadata当前类的注解信息paramregistry注册类,其registerBeanDefinition()可以注册beanOverridepublicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){1。从BeanIocScan注解获取到我们要搜索的包路径AnnotationAttributesannoAttrsAnnotationAttributes。fromMap(importingClassMetadata。getAnnotationAttributes(MyMapperScan。class。getName()));if(annoAttrsnullannoAttrs。isEmpty()){return;}String〔〕basePackages(String〔〕)annoAttrs。get(PACKAGENAMEKEY);2。找到指定包路径下所有添加了MapperBean注解的类,并且把这些类添加到IOC容器里面去ClassPathBeanDefinitionScannerscannernewClassPathBeanDefinitionScanner(registry,false);scanner。setResourceLoader(resourceLoader);路径包含MapperBean的注解的beanscanner。addIncludeFilter(newAnnotationTypeFilter(MapperBean。class));扫描包下路径scanner。scan(basePackages);}OverridepublicvoidsetResourceLoader(ResourceLoaderresourceLoader){this。resourceLoaderresourceLoader;}} 5)测试 这里扫描的路径就是上面User实体的位置RunWith(SpringRunner。class)SpringBootTestMyMapperScan(basePackages{com。jincou。importselector。mapperScan})publicclassUserServiceTest{AutowiredprivateUseruser;Testpublicvoidtest(){System。out。println(usernameuser。getClass()。getName());}} 运行结果usernamecom。jincou。importselector。mapperScan。User 完美,成功! 实现它的基本思想是:当自己需要操作BeanFactory里面的Bean的时候,那就必须只有它才能做到了。而且它还有个方便的地方,那就是做包扫描的时候,比如MapperScan类似这种的时候,用它处理更为方便(因为扫描到了直接注册即可)