首页 > 编程语言 >【JavaWeb】Spring Boot中@Import多种使用方式

【JavaWeb】Spring Boot中@Import多种使用方式

时间:2024-10-15 20:17:38浏览次数:8  
标签:String Spring Boot class ImportSelector Import 注解 public JavaWeb

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

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

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

一、普通静态类

定义实体

@Data
public class UserConfig {  
    /** 用户名*/
    private String username;

    /**手机号*/
    private String phone;
}

通过@Import注入容器

@Import(UserConfig.class)
@Configuration
public class UserConfiguration { 

}

也支持多个

@Import({UserConfig.class,UserConfig2.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);
    }
}

 导入带@Configuration的配置类:

@Configuration
public class MyConfig {
    // 配置内容
}
 
@Import(MyConfig.class)
@Configuration
public class AnotherConfig {
    // 配置内容
}

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

Bean方式注入:

@Configuration
public class UserConfiguration {

    @Bean
    public UserConfig userConfig() {
        return new UserConfig();
    }   
}

直接@Configuration配置注入

@Configuration
@Data
public class UserConfig {  
    /** 用户名*/
    private String username;

    /**手机号*/
    private String phone;
}

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

二、ImportSelector的实现类注入

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

public interface ImportSelector {

	String[] selectImports(AnnotationMetadata importingClassMetadata);
}

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

简单例子:

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.example.MyConfig"};
    }
}

Import直接使用:

@Import(MyImportSelector.class)
@Configuration
public class AnotherConfig {
    // 配置内容
}

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

多类直接注入

public class ModelImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{
                "com.springboot.demo.model.Animal",
                "com.springboot.demo.model.Car"
        };
    }
}
@SpringBootApplication
@Import(ModelImportSelector.class)
public class DemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run( DemoApplication.class, args );
        context.close();
    }
}

如果看到这,你肯定会有疑问。我这又是新建MyImportSelector类,又是实现ImportSelector重写selectImports方法,然后我这么做有个卵用呢?直接把类上加个@Component注入进去不香吗?这个ImportSelector把简单的功能搞这么复杂。

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

2.1 本地缓存与Redis缓存切换

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

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

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

public interface CacheService {
    
    void setData(String key);
}

2)本地缓存 实现类

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

3)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注入容器。

定义一个枚举

public enum CacheType {
    LOCAL, REDIS;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyCacheSelector.class)
public @interface EnableMyCache {
    CacheType type() default CacheType.REDIS;
}

4)、测试

这里选择本地缓存:

@EnableMyCache(type = CacheType.LOCAL)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private CacheService cacheService;

    @Test
    public void test() {
        cacheService.setData("key");
    }
}

 切换成redis缓存

@EnableMyCache(type = CacheType.REDIS)
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {

    @Autowired
    private CacheService cacheService;

    @Test
    public void test() {
        cacheService.setData("key");
    }
}

2.2 扫描包批量注入

这种方式实现了将我们想要交给Spring管理的类进行了托管,但是,如果有N多个类的话,这种写法会累死的,因此,需要通过递归的方式加载包路径名,然后统一初始化,将该包路径下的所有类进行湿实例化。首先我们自己创建一个注解:MyImport,并加入Spring的@Import注解,然后将启动类中的@Import注解改成我们自定义注解。

目的:扫描 "com.springboot.demo.model"下面的所有类,初始化并交给Spring容器管理

 自定义注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ModelImportSelector.class)
public @interface MyImport {
    String [] packages();
}

启动类加入自定义注解:

@SpringBootApplication
@MyImport(packages = "com.springboot.demo.model")
public class DemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run( DemoApplication.class, args );
        context.close();
    }
}

重写selectImports方法:

public class ModelImportSelector implements ImportSelector {

    private List<String> classList=new ArrayList <>();
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {

        //通过字节码文件解读,获取元素据信息,通过注解名称,返回注解所对应的属性的Map集合
        String [] packages = (String [])annotationMetadata.getAnnotationAttributes( MyImport.class.getName() ).get( "packages" );
        if (packages == null) {
            return null;
        }
        scanPackagesRecursion(packages);
        //获取注解中packages中配置的内容
        //获取需要扫描的包的所有类的路径
        return !classList.isEmpty()?classList.toArray( new String[classList.size()] ):null;
    }

    private void scanPackagesRecursion(String [] packages){
        for(String path : packages){
            doScanPackages(path);
        }
    }

    /**
     * 方法递归
     * @param path
     */
    private void doScanPackages(String path){
        URL resource = this.getClass().getClassLoader().
                getResource( path.replaceAll( "\\.", "/" ) );
        File file = new File( resource.getFile() );
        File[] files=file.listFiles();
        for (File fileSub : files) {
            if(fileSub.isDirectory()){
                doScanPackages(path+"."+fileSub.getName());
            }else{
                String fileName=fileSub.getName();
                System.out.println("fileName:"+fileName);
                if(fileName.endsWith( ".class" )){
                    String classPath=path+"."+fileName.replaceAll( "\\.class","" );
                    this.classList.add( classPath );
                    System.out.println("classPath:"+classPath);
                }
            }
        }
    }
}

2.3 Spring EnableAsync实现

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;
        }
    }
}

三、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) {

    }
}

3.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);
    }
}

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

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

我们来试下

@Import(MyImportBean.class)
@Configuration
public class UserImportConfiguration {

    /**
     * 这里通过@Bean注解,将UserConfig注入Spring容器中,而且名称也叫userConfig
     */
    @Bean
    public UserConfig userConfig() {
        return new UserConfig();
    }
}

 

3.2 Mybatis的@MapperScan

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

四、区别:

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

 

标签:String,Spring,Boot,class,ImportSelector,Import,注解,public,JavaWeb
From: https://blog.csdn.net/zhyooo123/article/details/142950555

相关文章

  • Java毕业设计 基于SpringBoot和Vue游戏商城网站
    Java毕业设计基于SpringBoot和Vue游戏商城网站这篇博文将介绍一个基于SpringBoot框架和Vue开发的游戏商城网站,适合用于Java毕业设计。功能介绍首页图片轮播游戏推荐游戏分类游戏详情添加购物车立即购买积分兑换评论收藏游戏论坛发布帖子游戏资讯......
  • springboot校园资产管理(11725)
     有需要的同学,源代码和配套文档领取,加文章最下方的名片哦一、项目演示项目演示视频二、资料介绍完整源代码(前后端源代码+SQL脚本)配套文档(LW+PPT+开题报告)远程调试控屏包运行三、技术介绍Java语言SSM框架SpringBoot框架Vue框架JSP页面Mysql数据库IDEA/Eclipse开发四、项......
  • springboot大学生科创项目在线管理系统(11744)
     有需要的同学,源代码和配套文档领取,加文章最下方的名片哦一、项目演示项目演示视频二、资料介绍完整源代码(前后端源代码+SQL脚本)配套文档(LW+PPT+开题报告)远程调试控屏包运行三、技术介绍Java语言SSM框架SpringBoot框架Vue框架JSP页面Mysql数据库IDEA/Eclipse开发四、项......
  • 基于springboot+微信小程序校园自助打印管理系统(打印1)
    ......
  • 通过 chatgpt 修复org.springframework:spring-webmvc 安全漏洞过程记录(chatgpt有时候
    1,首先我把这个安全漏洞的trivy完整描述send给了chatgpt并且随后把我的pom.xml也完整的send给了它。chatgpt给出的答案还算比较靠谱。 图一 图二 图三 图四 2,根据chatgpt的回复,我把<parent><groupId>org.springframework.boot</groupId><artifactId>sp......
  • RocketMq详解:五、SpringBoot+Aop实现RocketMq的幂等
    上一章:《RocketMq详解:四、RocketMq消息的重试》文章目录1什么是幂等2需要进行消息幂等的场景3.如何才能实现消息幂等呢4.RocketMQ场景下如何处理消息幂等4.1消费端常见的幂等操作1.使用唯一标识符2.Redis处理标志位3.分布式锁3.1数据库乐观锁3.2数据库悲观锁3.3Re......
  • java计算机毕业设计基于springboot的居家健身系统的设计与实现(开题+程序+论文)
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着现代生活节奏的加快,人们越来越注重健康与身体锻炼。然而,繁忙的工作和生活压力使得很多人难以抽出时间前往健身房进行锻炼。居家健身作为一种灵活......
  • [Spring] 深入理解: Spring @Value 解析、注入时机及原理
    内容摘要:@Value的使用及它是什么时候解析的并且解析后是如何注入值的?1@Value的使用简述@Value注解可用来将外部的值动态注入到Bean中,在@Value注解中,可以使${}与#{},它们的区别如下:(1)@Value("${}"):可以获取对应属性文件中定义的属性值。(2)@Value("#{}"):表示Sp......
  • IAR的boot + app类型的代码怎么用jlink仿真
    IAR的boot+app类型的代码怎么用jlink仿真1、2、3、仿真4、此时不要点击run5、用硬件restpin复位mcu6、当boot启动完毕后,刚刚跳转到app时,或者跳转到app前,执行行仿真注意,app的main需要自己初始化复位向量指针,修改到合适的中断向量入口指针如果你的代码没有rtos......
  • springboot官方文档回顾复习
    springboot官方文档回顾复习一、基础概念关于部署:使用Java-jar进行$java-jartarget/myapplication-0.0.1-SNAPSHOT.jar而打包后的jar程序,也支持通过命令行参数开启远程调试服务,如下:$java-Xdebug-Xrunjdwp:server=y,transport=dt_socket,address=8000,suspen......