简单两句

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

CSDN个人主页后端小知识

🔎GZH后端小知识

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

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

image-20231107004510265

说在前面

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

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

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

思维导图

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

image-20231107004253613

前言

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

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

  1. XML配置文件:在早期的Spring版本中,开发者主要通过XML配置文件来装配模块。在XML文件中,开发者可以定义beanbean之间关系以及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 {
}
声明老板类
public class Boss {
}

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({Boss.class})
public @interface EnableTavern {
}
创建配置类

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

@Configuration
@EnableTavern
public class TavernConfiguration {
}
编写测试类启动
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);
    }
}

运行结果如图

image-20231103010650347

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

导入配置类

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

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

声明调酒师类

调酒师类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Bartender {
    private String name;
}
注册调酒师对象

通过注解配置类的方式,可以一次性注册多个相同的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");
    }
}

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({Boss.class, BartenderConfiguration.class})
public @interface EnableTavern {
}
测试运行
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);
        });
    }
}

运行结果如图

image-20231103012517338

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

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

导入ImportSelector

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

声明吧台类+配置类

声明吧台类

public class Bar {
}

BarConfiguration配置类中注册Bar

@Configuration
public class BarConfiguration {
    @Bean
    public Bar bbar(){
        return new Bar();
    }
}

编写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()};
    }
}

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
public @interface EnableTavern {
}
测试运行
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);
        });
    }
}

image-20231106234621568

ImportSelector的灵活性

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

导入ImportBeanDefinitionRegistrar

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

声明服务员类

服务员类

public class Waiter {
}
编写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));
    }
}

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
public @interface EnableTavern {
}
测试运行

测试代码

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

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

写好后,同样在@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 {
}

测试看控制打印

image-20231107001224904

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

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

抽象工厂  引导关注

结语

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

💬

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

CSDN个人主页后端小知识

🔎GZH后端小知识

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

原文地址:https://blog.csdn.net/m0_46833224/article/details/134271469

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_50091.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注