• 详细解读Spring Boot中@Import三种使用方式


    需要注意的是:ImportSelector、ImportBeanDefinitionRegistrar这两个接口都必须依赖于@Import一起使用,而@Import可以单独使用。

    @Import是一个非常有用的注解,它的长处在于你可以通过配置来控制是否注入该Bean,也可以通过条件来控制注入哪些Bean到Spring容器中。

    比如我们熟悉的:@EnableAsync 、@EnableCaching@EnableScheduling等等统一采用的都是借助@Import注解来实现的。

    下面我们就通过示例来了解@Import三种用法!

    一、引入普通类

    有个用户类如下

    1. @Data
    2. public class UserConfig {
    3. /** 用户名*/
    4. private String username;
    5. /**手机号*/
    6. private String phone;
    7. }

    那么如何通过@Import注入容器呢?

    1. @Import(UserConfig.class)
    2. @Configuration
    3. public class UserConfiguration {
    4. }

    当在@Configuration标注的类上使用@Import引入了一个类后,就会把该类注入容器中。

    当然除了@Configuration 比如@Component、@Service等一样也可以。

    测试

    1. @SpringBootTest
    2. @RunWith(SpringRunner.class)
    3. public class UserServiceTest {
    4. @Autowired
    5. private UserConfig userConfig;
    6. @Test
    7. public void getUser() {
    8. String name = userConfig.getClass().getName();
    9. System.out.println("name = " + name);
    10. }
    11. }

    控制台输出

    name = com.jincou.importselector.model.UserConfig

    如果@Import的功能仅仅是这样,那其实它并没什么特别的价值,我们可以通过其它方式实现?

    1. @Configuration
    2. public class UserConfiguration {
    3. @Bean
    4. public UserConfig userConfig() {
    5. return new UserConfig();
    6. }
    7. }

    再比如直接添加@Configuration注解

    1. @Configuration
    2. public class UserConfig {
    3. // ......
    4. }

    确实如果注入静态的Bean到容器中,那完全可以用上面的方式代替,但如果需要动态的带有逻辑性的注入Bean,那才更能体现@Import的价值。

    二、引入ImportSelector的实现类

    说到ImportSelector这个接口就不得不说这里面最重要的一个方法:selectImports()

    1. public interface ImportSelector {
    2. String[] selectImports(AnnotationMetadata importingClassMetadata);
    3. }

    这个方法的返回值是一个字符串数组,只要在配置类被引用了,这里返回的字符串数组中的类名就会被Spring容器new出来,然后再把这些对象注入IOC容器中。

    所以这有啥用呢?我们还是用一个例子演示一下。

    1、静态import场景(注入已知的类)

    我们先将上面的示例改造下:

    自定义MyImportSelector实现ImportSelector接口,重写selectImports方法

    1. public class MyImportSelector implements ImportSelector {
    2. @Override
    3. public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    4. //这里目的是将UserConfig 注入容器中
    5. return new String[]{"com.jincou.importselector.model.UserConfig"};
    6. }
    7. }

    然后在配置类引用

    1. @Import(MyImportSelector.class)
    2. @Configuration
    3. public class UserConfiguration {
    4. }

    这样一来同样可以通过成功将UserConfig注入容器中。

    如果看到这,你肯定会有疑问。我这又是新建MyImportSelector类,又是实现ImportSelector重写selectImports方法,然后我这么做有个卵用呢?

    直接把类上加个@Component注入进去不香吗?这个ImportSelector把简单的功能搞这么复杂。

    接下来就要说说如何动态注入Bean了。

    2、动态import场景(注入指定条件的类)

    我们来思考一种场景,就是你想通过开关来控制是否注入该Bean,或者说通过配置来控制注入哪些Bean,这个时候就有了ImportSelector的用武之地了。

    我们来举个例子,通过ImportSelector的使用实现条件选择是注入本地缓存还是Redis缓存

    1)、定义缓存接口和实现类

    顶层接口

    1. public interface CacheService {
    2. void setData(String key);
    3. }

    本地缓存 实现类

    1. public class LocalServicempl implements CacheService {
    2. @Override
    3. public void setData(String key) {
    4. System.out.println("本地存储存储数据成功 key= " + key);
    5. }
    6. }

    redis缓存实现类

    1. public class RedisServicempl implements CacheService {
    2. @Override
    3. public void setData(String key) {
    4. System.out.println("redis存储数据成功 key= " + key);
    5. }
    6. }

    2)、定义ImportSelector实现类

    以下代码中根据EnableMyCache注解中的不同值来切换缓存的实现类再spring中的注册。

    1. public class MyCacheSelector implements ImportSelector {
    2. @Override
    3. public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    4. Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableMyCache.class.getName());
    5. //通过 不同type注入不同的缓存到容器中
    6. CacheType type = (CacheType) annotationAttributes.get("type");
    7. switch (type) {
    8. case LOCAL: {
    9. return new String[]{LocalServicempl.class.getName()};
    10. }
    11. case REDIS: {
    12. return new String[]{RedisServicempl.class.getName()};
    13. }
    14. default: {
    15. throw new RuntimeException(MessageFormat.format("unsupport cache type {0}", type.toString()));
    16. }
    17. }
    18. }
    19. }

    3)、定义注解

    @EnableMyCache注解就像一个开关,通过这个开关来是否将特定的Bean注入容器。

    定义一个枚举

    1. @Target(ElementType.TYPE)
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Import(MyCacheSelector.class)
    5. public @interface EnableMyCache {
    6. CacheType type() default CacheType.REDIS;
    7. }
    8. public enum CacheType {
    9. LOCAL, REDIS;
    10. }

    4)、测试

    这里选择本地缓存。

    1. @EnableMyCache(type = CacheType.LOCAL)
    2. @SpringBootTest
    3. @RunWith(SpringRunner.class)
    4. public class UserServiceTest {
    5. @Autowired
    6. private CacheService cacheService;
    7. @Test
    8. public void test() {
    9. cacheService.setData("key");
    10. }
    11. }

    控制台输出

    本地存储存储数据成功 key= key

    切换成redis缓存

    1. @EnableMyCache(type = CacheType.REDIS)
    2. @SpringBootTest
    3. @RunWith(SpringRunner.class)
    4. public class UserServiceTest {
    5. @Autowired
    6. private CacheService cacheService;
    7. @Test
    8. public void test() {
    9. cacheService.setData("key");
    10. }
    11. }

    控制台输出

    redis存储数据成功 key= key

    这个示例不是就是Bean的动态注入了吗?

    3、Spring如何使用ImportSelector的场景

    SpringBoot有两个常用注解 @EnableAsync @EnableCaching 其实就是通过ImportSelector来动态注入Bean

    看下@EnableAsync注解,它有通过@Import({AsyncConfigurationSelector.class})

    1. @Target({ElementType.TYPE})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Import({AsyncConfigurationSelector.class})
    5. public @interface EnableAsync {
    6. Class annotation() default Annotation.class;
    7. boolean proxyTargetClass() default false;
    8. AdviceMode mode() default AdviceMode.PROXY;
    9. int order() default 2147483647;
    10. }

    AsyncConfigurationSelector.class

    1. public class AsyncConfigurationSelector extends AdviceModeImportSelector {
    2. private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";
    3. public AsyncConfigurationSelector() {
    4. }
    5. @Nullable
    6. public String[] selectImports(AdviceMode adviceMode) {
    7. switch(adviceMode) {
    8. case PROXY:
    9. return new String[]{ProxyAsyncConfiguration.class.getName()};
    10. case ASPECTJ:
    11. return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
    12. default:
    13. return null;
    14. }
    15. }
    16. }

    是不是和我上面写的示例一样。

    总之,向这种还不能决定去注入哪个处理器(如果你能决定,那就直接@Import那个类好了,没必要实现接口了),就可以实现此接口,写出一些判断逻辑,不同的配置情况注入不同的处理类。

    三、引入ImportBeanDefinitionRegister的实现类

    当配置类实现了 ImportBeanDefinitionRegistrar 接口,你就可以自定义往容器中注册想注入的Bean。

    这个接口相比与 ImportSelector 接口的主要区别就是,ImportSelector接口是返回一个类,你不能对这个类进行任何操作,但是 ImportBeanDefinitionRegistrar 是可以自己注入 BeanDefinition,可以添加属性之类的。

    1. public class MyImportBean implements ImportBeanDefinitionRegistrar {
    2. /**
    3. * @param importingClassMetadata 当前类的注解信息
    4. * @param registry 注册类,其registerBeanDefinition()可以注册bean
    5. */
    6. @Override
    7. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    8. }
    9. }

    1、举一个简单的示例

    我们通过先通过一个简单的小示例,来理解它的基本使用

    假设有个用户配置类如下

    1. @Data
    2. public class UserConfig {
    3. /** 用户名*/
    4. private String username;
    5. /**手机号*/
    6. private String phone;
    7. }

    我们通过实现ImportBeanDefinitionRegistrar的方式来完成注入。

    1. public class MyImportBean implements ImportBeanDefinitionRegistrar {
    2. /**
    3. * @param importingClassMetadata 当前类的注解信息
    4. * @param registry 注册类,其registerBeanDefinition()可以注册bean
    5. */
    6. @Override
    7. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    8. //构建一个 BeanDefinition , Bean的类型为 UserConfig,这个Bean的属性username的值为后端元宇宙
    9. AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserConfig.class)
    10. .addPropertyValue("username", "后端元宇宙")
    11. .getBeanDefinition();
    12. //把 UserConfig 这个Bean的定义注册到容器中
    13. registry.registerBeanDefinition("userConfig", beanDefinition);
    14. }
    15. }

    通过配置类 中引入MyImportBean对象。

    1. @Import(MyImportBean.class)
    2. @Configuration
    3. public class UserImportConfiguration {
    4. }

    我们再来测试下

    1. @EnableMyCache(type = CacheType.REDIS)
    2. @SpringBootTest
    3. @RunWith(SpringRunner.class)
    4. public class UserServiceTest {
    5. @Autowired
    6. private UserConfig userConfig;
    7. @Test
    8. public void test() {
    9. String username = userConfig.getUsername();
    10. System.out.println("username = " + username);
    11. }
    12. }

    控制台输出

    username = 后端元宇宙

    说明通过ImportBeanDefinitionRegistrar方式,已经把UserConfig注入容器成功,而且还为给bean设置了新属性。

    然后我们再来思考一个问题,就比如我们在其它地方已经将UserConfig注入容器,这里会不会出现冲突,或者不冲突的情况下,属性能不能设置成功?

    我们来试下

    1. @Import(MyImportBean.class)
    2. @Configuration
    3. public class UserImportConfiguration {
    4. /**
    5. * 这里通过@Bean注解,将UserConfig注入Spring容器中,而且名称也叫userConfig
    6. */
    7. @Bean
    8. public UserConfig userConfig() {
    9. return new UserConfig();
    10. }
    11. }

    然后我们再来跑下上面的测试用例,发现报错了。

    2、举一个复杂点的例子

    Mybatis的@MapperScan就是用这种方式实现的,@MapperScan注解,指定basePackages,扫描Mybatis Mapper接口类注入到容器中。

    这里我们自定义一个注解@MyMapperScan来扫描包路径下所以带@MapperBean注解的类,并将它们注入到IOC容器中。

    1)、先定义一个@MapperBean注解,就相当于我们的@Mapper注解

    1. /**
    2. * 定义包路径。(指定包下所有添加了MapperBean注解的类作为bean)
    3. * 注意这里 @Import(MyMapperScanImportBean.class) 的使用
    4. */
    5. @Retention(RetentionPolicy.RUNTIME)
    6. @Target(ElementType.TYPE)
    7. @Documented
    8. public @interface MapperBean {
    9. }

    2)、一个需要注入的bean,这里加上@MapperBean注解。

    1. package com.jincou.importselector.mapperScan;
    2. import com.jincou.importselector.config.MapperBean;
    3. @MapperBean
    4. public class User {
    5. }

    3)、再定一个扫描包路径的注解@MyMapperScan 就相当于mybatis的@MapperScan注解。

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.TYPE)
    3. @Documented
    4. @Import(MyMapperScanImportBean.class)
    5. public @interface MyMapperScan {
    6. /**
    7. * 扫描包路径
    8. */
    9. String[] basePackages() default {};
    10. }

    4)、MyMapperScanImportBean实现ImportBeanDefinitionRegistrar接口

    1. public class MyMapperScanImportBean implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
    2. private final static String PACKAGE_NAME_KEY = "basePackages";
    3. private ResourceLoader resourceLoader;
    4. /**
    5. * 搜索指定包下所有添加了MapperBean注解的类,并且把这些类添加到ioc容器里面去
    6. *
    7. * @param importingClassMetadata 当前类的注解信息
    8. * @param registry 注册类,其registerBeanDefinition()可以注册bean
    9. */
    10. @Override
    11. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    12. //1. 从BeanIocScan注解获取到我们要搜索的包路径
    13. AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MyMapperScan.class.getName()));
    14. if (annoAttrs == null || annoAttrs.isEmpty()) {
    15. return;
    16. }
    17. String[] basePackages = (String[]) annoAttrs.get(PACKAGE_NAME_KEY);
    18. // 2. 找到指定包路径下所有添加了MapperBean注解的类,并且把这些类添加到IOC容器里面去
    19. ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
    20. scanner.setResourceLoader(resourceLoader);
    21. //路径包含MapperBean的注解的bean
    22. scanner.addIncludeFilter(new AnnotationTypeFilter(MapperBean.class));
    23. //扫描包下路径
    24. scanner.scan(basePackages);
    25. }
    26. @Override
    27. public void setResourceLoader(ResourceLoader resourceLoader) {
    28. this.resourceLoader = resourceLoader;
    29. }
    30. }

    5)测试

    这里扫描的路径就是上面User实体的位置

    1. @RunWith(SpringRunner.class)
    2. @SpringBootTest
    3. @MyMapperScan(basePackages = {"com.jincou.importselector.mapperScan"})
    4. public class UserServiceTest {
    5. @Autowired
    6. private User user;
    7. @Test
    8. public void test() {
    9. System.out.println("username = " + user.getClass().getName());
    10. }
    11. }

    运行结果

    username = com.jincou.importselector.mapperScan.User

    完美,成功!

    实现它的基本思想是:当自己需要操作BeanFactory里面的Bean的时候,那就必须只有它才能做到了。而且它还有个方便的地方,那就是做包扫描的时候,比如@MapperScan类似这种的时候,用它处理更为方便(因为扫描到了直接注册即可)

     

  • 相关阅读:
    【Kali安全渗透测试实践教程】第8章 Web渗透
    走出心理舒适区的七个最佳方法
    springboot基于微信小程序的在线办公系统+java+uinapp+Mysql+计算机毕业设计
    计算机毕业设计springboot+vue基本微信小程序的家装公司管理系统小程序
    基于ASP.Net Core和Layui实现简单用户登录
    python基础语法--列表
    chatgpt赋能python:Python随机选择数字
    Vue面试题
    无限猴子 歌唱王国 题解
    mybatis 动态sql和分页
  • 原文地址:https://blog.csdn.net/wdj_yyds/article/details/127996716