• 揭秘Spring框架:模块装配的奥秘与实战技巧 【Spring|Java】


    简单说两句

    作者:后端小知识CSDN后端领域新星创作者|阿里云专家博主

    CSDN个人主页后端小知识

    🔎GZH后端小知识

    🎉欢迎关注🔎点赞👍收藏⭐️留言📝

    揭秘Spring框架:模块装配的奥秘与实战技巧


    image-20231107004510265

    说在前面

    本系列文章是Spring学习笔记,参考书**《SpringBoot源码解读与原理分析》**

    阅读的一个小要求:至少会使用SpringBoot或Spring或者天才型选手

    本系列文章将会持续更新更新速度取决于各位读者大大的点赞收藏和留言

    思维导图

    为了方便读者了解文章整体内容,我给出了一张思维导图,希望有所帮助

    image-20231107004253613

    前言

    Spring框架是一个轻量级的Java企业级应用开发框架,它提供了一种简化企业级应用开发的方法。Spring框架的核心是依赖注入(DI)和面向切面编程(AOP),这两个特性使得开发者可以更容易地构建和管理复杂的企业级应用。

    在Spring框架中,模块装配主要是指将各个模块(组件)组合在一起,形成一个完整的应用。Spring框架提供了多种方式来实现模块装配,以下是一些常见的方法:

    1. XML配置文件:在早期的Spring版本中,开发者主要通过XML配置文件来装配模块。在XML文件中,开发者可以定义bean、bean之间的关系以及bean的属性等。这种方式虽然灵活,但随着项目规模的增大,XML配置文件可能会变得非常复杂,难以维护。
    2. 注解:从Spring 2.5版本开始,Spring框架引入了注解支持,使得开发者可以通过注解来简化模块装配。常见的注解有@Component(标识一个受Spring IOC容器管理的普通组件)、@Repository(标识一个受Spring IOC容器管理的持久化层组件)、@Service(标识一个受Spring IOC容器的业务逻辑层组件)和@Controller(标识一个受Spring IOC容器管理的表述层控制器组件)。通过使用这些注解,开发者可以直接在类上进行装配,而无需编写繁琐的XML配置文件。
    3. Java配置:从Spring 3.0版本开始,Spring框架引入了Java配置支持,允许开发者使用Java代码来定义bean和bean之间的关系。这种方式相较于XML配置更加简洁,易于阅读和维护。开发者可以使用@Configuration注解来定义配置类,然后在配置类中使用@Bean注解来定义bean。
    4. 自动装配:Spring框架提供了自动装配功能,可以根据类型或名称自动将bean装配到其他bean中。这可以大大简化模块装配的过程。开发者可以使用@Autowired、@Qualifier和@Value等注解来实现自动装配。

    Spring框架提供了多种模块装配方式,开发者可以根据项目需求和团队习惯选择合适的方式进行模块装配。随着Spring框架的不断发展,模块装配的方式也在不断简化,使得开发者可以更加专注于业务逻辑的实现。

    模块

    在Spring框架中,模块(Module)通常是指一个功能独立的组件,它负责处理特定的业务逻辑或提供特定的功能。模块可以是一个类、一个服务接口、一个数据访问对象(DAO)等。模块之间可以通过依赖注入(DI)和面向切面编程(AOP)等技术进行组合和扩展,从而构建出一个完整的企业级应用。

    在Spring框架中,模块通常具有以下特点:

    1. 功能独立:模块应该具有明确的功能边界,负责处理特定的业务逻辑或提供特定的功能。这有助于降低模块之间的耦合度,提高代码的可维护性和可扩展性。
    2. 可重用性:模块应该具有良好的可重用性,可以在不同的项目或应用中进行复用。这有助于减少代码的重复编写,提高开发效率。
    3. 可测试性:模块应该具有良好的可测试性,可以方便地进行单元测试和集成测试。这有助于确保模块的正确性和稳定性。
    4. 可扩展性:模块应该具有良好的可扩展性,可以通过继承、组合等方式进行扩展。这有助于应对业务需求的变化,提高应用的灵活性。

    在Spring框架中,模块通常通过依赖注入(DI)和面向切面编程(AOP)等技术进行组合和扩展。依赖注入允许模块之间通过依赖关系进行通信,而面向切面编程则允许在模块之间添加横切关注点,如日志记录、性能监控等。这些技术使得模块之间的组合和扩展变得更加灵活和方便。

    快速体会模块装配

    场景

    假设一个场景:使用代码模拟构建一个酒馆,酒馆里面有吧台、调酒师、服务员和老板4种不同的实体元素,在该场景中,酒馆可以看作ApplicationContext,吧台、调酒师、服务员和老板可以看作组件,使用代码模拟实现的最终目的,可以通过一个注解,把以上元素全部填充到酒馆中

    【Tips】:假设的场景仅配合代码完成演示

    声明自定义注解

    声明一个注解:@EnableTavern

    EnableTavern代码

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface EnableTavern {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    声明老板类
    public class Boss {
    }
    
    • 1
    • 2

    声明好了后,我们在EnableTavern中写上@Import注解并填入Boss类,如下代码所示,这就意味着如果一个配置类上标注了@EnableTavern注解的话,就会触发@Import的效果,向容器中导入一个Boss类的Bean

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Import({Boss.class})
    public @interface EnableTavern {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    创建配置类

    注解驱动的测试离不开配置类,下面声明一个TavernConfiguration配置类,并标注@Configuration和@EnableTavern注解

    @Configuration
    @EnableTavern
    public class TavernConfiguration {
    }
    
    • 1
    • 2
    • 3
    • 4
    编写测试类启动
    public class TavernAppTest {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Boss bean = ctx.getBean(Boss.class);
            System.out.println("bean = " + bean);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果如图

    image-20231103010650347

    看运行结果,我们可以发现使用getBean能够正常提取Boss对象,说明Boss类已经被注册到IOC容器中了,并且创建了一个对象,到这里就完成了最简单的模块装配

    导入配置类

    到这里可能有读者会产生疑惑,原本通过@Configuration加@Bean注解就能完成的工作。换用@Import注解后代码量却增加了。这不是徒增功耗吗?如果你也有这种疑问请,不要着急,仔细观察@Import的value属性允许传入的类。可以发现普通类似最简单的方式,而其余几种类型更为重要。

    如果需要直接导入项目中现有的一些配置类使用@Import也可以直接加载进来。

    声明调酒师类

    调酒师类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Bartender {
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    注册调酒师对象

    通过注解配置类的方式,可以一次性注册多个相同的bean对象,下面编写一个配置类BartenderConfiguration,并用@Bean注册两个不同的Bartender类

    注册调酒师的配置类 BartenderConfiguration

    @Configuration
    public class BartenderConfiguration {
    
        @Bean
        public Bartender getBartender(){
            return new Bartender("调酒师-01");
        }
    
        @Bean
        public Bartender getBartender02(){
            return new Bartender("调酒师-02");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在@EnableTavern 注解中添加BartenderConfiguration配置类

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Import({Boss.class, BartenderConfiguration.class})
    public @interface EnableTavern {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    测试运行
    public class TavernAppTest {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
            System.out.println("================================");
            Map<String, Bartender> beansOfType = ctx.getBeansOfType(Bartender.class);
            beansOfType.forEach((name,bartender) ->{
                System.out.println("name = " + name);
                System.out.println("bartender = " + bartender);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果如图

    image-20231103012517338

    通过看运行结果,可以发现控制台成功打印两个调酒师对象,说明注解配置类的装配正确完成。

    【Tips】:注意,BartenderConfiguration配置类也被注册到了IOC容器中,并成为一个Bean

    导入ImportSelector

    通过IDEA可以看到ImportSelector是一个接口,这个接口可以导入配置类,也可以导入普通类

    声明吧台类+配置类

    声明吧台类

    public class Bar {
    }
    
    • 1
    • 2

    BarConfiguration配置类中注册Bar

    @Configuration
    public class BarConfiguration {
        @Bean
        public Bar bbar(){
            return new Bar();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    编写ImportSelector的实现类

    BarImportSelector实现ImportSelector接口

    public class BarImportSelector implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
            System.out.println("BarImportSelector invoke ...");
            return new String[]{Bar.class.getName(), BarConfiguration.class.getName()};
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    写好后,在@EnableTavern注解中添加BarImportSelector

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
    public @interface EnableTavern {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    测试运行
    public class TavernAppTest {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
            System.out.println("================================");
            Map<String, Bartender> beansOfType = ctx.getBeansOfType(Bartender.class);
            beansOfType.forEach((name,bartender) ->{
                System.out.println("name = " + name);
                System.out.println("bartender = " + bartender);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20231106234621568

    ImportSelector的灵活性

    ImportSelector的核心是可以使开发者采用更灵活的声明式向IOC容器中注册bean,其重点是可以灵活的指定要注册的Bean的类,正因为传入的是全限定名的字符串那么如果这些全限定名以配置文件的形式存放在项目可以读取的位置,是不是就可以避免导入组件的硬编码问题了在SpringBoot自动装配中,底层就是利用了ImportSelector,实现从spring.factories文件中读取自动配置类。

    导入ImportBeanDefinitionRegistrar

    如果说ImportSelector是以声明式导入组件,那么ImportBeanDefinitionRegistrar可以解释为以编程式向IOC容器中注册Bean对象,实际导入的是BeanDefinition(Bean的定义信息)

    声明服务员类

    服务员类

    public class Waiter {
    }
    
    • 1
    • 2
    编写ImportBeanDefinitionRegistrar的实现类

    WaiterRegistrar实现ImportBeanDefinitionRegistrar

    public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            System.out.println("WaiterRegistrar invoke ....");
            registry.registerBeanDefinition("waiter", new RootBeanDefinition(Waiter.class));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在@EnableTavern注解中添加ImportBeanDefinitionRegistrar的方式

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
    public @interface EnableTavern {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    测试运行

    测试代码

    public class TavernAppTest {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
            System.out.println("================================");
            Map<String, Bartender> beansOfType = ctx.getBeansOfType(Bartender.class);
            beansOfType.forEach((name,bartender) ->{
                System.out.println("name = " + name);
                System.out.println("bartender = " + bartender);
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20231107000338828

    【Tips】: WaiterRegistrar没有被注册到IOC容器中

    DeferredImportSelector(扩展)

    DeferredImportSelector 是 Spring 提供的一个接口(ImportSelector的子接口),用于在运行时动态导入额外的配置类。它与 AutoConfigurationImportSelector 的作用类似,但在应用程序初始化阶段不导入所有配置,而是在需要时才导入额外的配置。使用 DeferredImportSelector 需要创建实现该接口的类,并在应用程序中通过 @Import 注解引入。通过实现 selectImports 方法,可以根据当前应用程序环境和需求动态选择和导入额外的配置类。

    实现 DeferredImportSelector 可以提高应用程序的启动速度和效率,因为只在需要时加载额外的配置。这对于特定的应用程序场景是非常有用的,例如,当您需要根据不同的环境或配置选项加载额外的配置时,或在某些情况下懒惰加载配置以提高启动速度。

    DeferredImportSelector 还可以通过实现排序接口,在导入额外的配置时按照特定的顺序进行排序,以便确保额外的配置在正确的顺序中加载。总的来说,DeferredImportSelector 是一种强大的工具,可以提高应用程序的性能和可维护性,并使您能够更加灵活地管理和导入额外的配置。

    DeferredImportSelector执行时机

    WaiterDeferredImportSelector导入服务员类

    public class WaiterDeferredImportSelector implements DeferredImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
            System.out.println("WaiterDeferredImportSelector invoke ....");
            return new String[]{Waiter.class.getName()};
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    写好后,同样在@EnableTavern注解中的@Import上添加WaiterDeferredImportSelector的导入

    EnableTavern注解

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class, WaiterDeferredImportSelector.class})
    public @interface EnableTavern {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试看控制的打印

    image-20231107001224904

    可以发现,**DeferredImportSelector的执行时机比ImportSelector的晚,比ImportBeanDefinitionRegistrar早,**为什么要这么设计,可以看看后面的Condition条件装配

    【都看到这了,点点赞点点关注呗,爱你们】😚😚

    抽象工厂  引导关注

    结语

    谢谢你的阅读,由于作者水平有限,难免有不足之处,若读者发现问题,还请批评,在留言区留言或者私信告知,我一定会尽快修改的。若各位大佬有什么好的解法,或者有意义的解法都可以在评论区展示额,万分谢谢。
    写作不易,望各位老板点点赞,加个关注!😘😘😘

    💬

    作者:后端小知识CSDN后端领域新星创作者|阿里云专家博主

    CSDN个人主页后端小知识

    🔎GZH后端小知识

    🎉欢迎关注🔎点赞👍收藏⭐️留言📝

  • 相关阅读:
    electron中的webview、iframe、BrowserView哪个好?如何选择(一)
    Apache Druid连接回收引发的血案
    我看世界杯
    Echarts柱状图配置代码详解,含常用图例代码
    【青书学堂】2023年第二学期 PhotoShop基础与应用(高起专) 作业
    小程序使用腾讯位置插件获取当前位置
    Mathorcup数学建模竞赛第四届-【妈妈杯】C题:家庭暑假旅游套餐的设计
    远程办公时意外摔伤,算工伤吗?
    TikTok选品精华 不能错过的TikTok选品3大原则+方法
    【SpringBoot高级篇】SpringBoot: 事件的发布和监听
  • 原文地址:https://blog.csdn.net/m0_46833224/article/details/134271469