美文网首页@IT·互联网
SpringBoot源码解读与原理分析(二)组件装配

SpringBoot源码解读与原理分析(二)组件装配

作者: 灰色孤星 | 来源:发表于2023-12-31 23:25 被阅读0次

    SpringBoot源码解读与原理分析(合集)

    2.1 组件装配

    2.1.1 组件

    组件:IOC容器中的核心API对象
    组件装配:将核心API配置到XML配置文件或注解配置类的行为
    Spring Framework 只有一种组件装配方式,即手动装配;而 Spring Boot 基于原生的手动装配,通过模块装配+条件装配+SPI机制,完美实现组件的自动装配。

    2.1.2 手动装配

    手动装配,是指开发者在项目中通过编写XML配置文件、注解配置类、配合特定注解等方式,将所需的组件注册到IOC容器(即ApplicationContext)中。
    三种手动装配方式(共性:需要手动编写配置信息):

    <!-- 基于XML配置文件的手动配置 -->
    <bean id="person" class="com.xiaowd.springboot.component.Person"/>
    
    // 基于注解配置类的手动装配
    @Configuration
    public class ExampleConfiguration {
        @Bean
        public Person person() {
            return new Person();
        }
    }
    
    // 基于组件扫描的手动装配
    @Component
    public class DemoService {
    }
    @Configuration
    @ComponentScan("com.xiaowd.springboot")
    public class ExampleConfiguration {
    }
    

    2.1.3 自动装配

    自动装配是 Spring Boot 的核心特性之一。
    自动装配:本应该由开发者编写的配置,转为框架自动根据项目中整合的场景依赖,合理地做出判断并装配合适的Bean到IOC容器中。相比较于手动装配,自动装配关注的重点是整合的场景,而不是每个具体的场景中所需的组件。

    • 实现机制:模块装配+条件装配+SPI机制
    • 非侵入性:默认注册的组件可以被覆盖。如整个spring-jdbc时,如果项目中已经注册了JdbcTemplate,则SpringBoot提供的默认的JdbcTemplate就不会再创建。
    • 配置禁用:在@SpringBootApplication或者@EnableAutoConfiguration注解上标注exclude/excludeName属性,可以禁用默认的自动配置类;或者在全局配置文件中声明spring.autoconfigure.exclude属性。

    2.2 Spring Framework的模块装配

    模块装配是自动装配的核心,可以把一个模块所需的核心功能组件都装配到IOC容器中。
    通过标注@EnableXXX注解,实现快速激活和装配对应的模块

    2.2.1 模块

    • 独立的:一个个可以分解、组合、更换的独立单元
    • 功能高内聚:一个模块通常用于解决一个独立的问题
    • 可相互依赖:模块间
    • 目标明确

    2.2.2 模块装配举例

    模块装配的核心原则:自定义注解+@Import导入组件

    1.模块装配场景

    使用代码模拟构建一个酒馆,酒馆里有吧台、调酒师、服务员和老板4种不同的实体元素;酒馆可以看成IOC容器,4种不同的实体元素可以看成4个组件。
    目的:通过一个注解,把以上元素全部填充到酒馆中。

    2.声明自定义注解@EnableTavern

    @Documented
    @Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
    @Target(ElementType.TYPE) // 该注解只能标注到类上
    public @interface EnableTavern {
    }
    

    3.声明老板类Boss

    public class Boss {
    }
    

    4.在@EnableTavern增加@Import注解

    @Import注解源码如下:


    由源码可知,@Import注解可以导入配置类、ImportSelector的实现类、ImportBeanDefinitionRegistrar的实现类,以及普通类。
    接下来在@EnableTavern的@Import注解中填入Boss类,这就意味着如果一个配置类上标注了@EnableTavern注解,就会触发@Import的效果,向容器中导入一个Boss类的Bean。
    @Documented
    @Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
    @Target(ElementType.TYPE) // 该注解只能标注到类上
    @Import(Boss.class)
    public @interface EnableTavern {
    
    }
    

    5.创建配置类

    @Configuration
    @EnableTavern
    public class TavernConfiguration {
    }
    

    6.编写启动类测试

    public class TavernApplication {
    
        public static void main(String[] args) {
            AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Boss boss = ctx.getBean(Boss.class);
            System.out.println(boss);
        }
    
    }
    

    运行结果显示,使用getBean可以正常获取Boss对象,说明Boss类已经被注册到了IOC容器,并创建了一个对象。

    2.2.3 导入配置类

    1.声明调酒师类

    public class Bartender {
        
        private String name;
    
        public Bartender(String name) {
            this.name = name;
        }
    
        // getter and setter
    }
    

    2.声明注解配置类

    @Configuration
    public class BartenderConfiguration {
        
        @Bean
        public Bartender zhangsan() {
            return new Bartender("张三");
        }
    
        @Bean
        public Bartender lisi() {
            return new Bartender("李四");
        }
        
    }
    

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

    @Documented
    @Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
    @Target(ElementType.TYPE) // 该注解只能标注到类上
    @Import({Boss.class, BartenderConfiguration.class})
    public @interface EnableTavern {
    
    }
    

    4.测试运行

    public class TavernApplication {
    
        public static void main(String[] args) {
            AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Map<String, Bartender> bartenders = ctx.getBeansOfType(Bartender.class);
            bartenders.forEach((name, bartender) -> System.out.println(name, bartender));
        }
    
    }
    

    运行结果显示,两个调酒师对象已经注册到了IOC容器。
    注意:
    配置类@Configuration还可以被组件扫描(ComponentScan)识别到,如果配置了组件扫描,不使用@Import导入配置类也可以在IOC容器中找到相应的组件。另外,本例中BartenderConfiguration本身也被注册到了IOC容器中成为一个Bean。

    2.2.4 导入ImportSelector实现类

    1.ImportSelector源码

    Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.
    ImportSelector是一个接口,它的实现类可以根据指定的筛选标准(通常是一个或多个注解)来决定那些配置类被导入。
    被ImportSelector导入的类,最终会在IOC容器中以单实例Bean的形式创建并保存。

    2.声明吧台类

    public class Bar {
    }
    

    3.声明配置类

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

    4.编写ImportSelector的实现类

    public class BarImportSelector implements ImportSelector {
    
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
        }
    
    }
    

    selectImports方法源码:

    Select and return the names of which class(es) should be imported based on the AnnotationMetadata of the importing @Configuration class.
    Returns: the class names, or an empty array if none
    根据导入的@Configuration类的注解元数据AnnotationMetadata选择并返回要导入的类的类名。
    注意:返回的一组类名一定是全限定类名(可直接定位)

    5.在@EnableTavern注解中添加BarImportSelector

    @Documented
    @Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
    @Target(ElementType.TYPE) // 该注解只能标注到类上
    @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
    public @interface EnableTavern {
    
    }
    

    6.测试运行

    public class TavernApplication {
    
        public static void main(String[] args) {
            AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Map<String, Bar> bars = ctx.getBeansOfType(Bar.class);
            bars.forEach((name, bar) -> System.out.println(name));
            System.out.println("=======");
            Map<String, BarConfiguration> barConfigurations = ctx.getBeansOfType(BarConfiguration.class);
            barConfigurations.forEach((name, barConfiguration) -> System.out.println(name));
            System.out.println("=======");
            Map<String, BarImportSelector> barImportSelectors = ctx.getBeansOfType(BarImportSelector.class);
            barImportSelectors.forEach((name, barImportSelector) -> System.out.println(name));
            System.out.println("=======");
        }
    
    }
    

    运行结果显示:
    ImportSelector可以导入普通类(Bar),可以导入配置类(BarConfiguration),但没有导入BarImportSelector。

    7.ImportSelector的灵活性

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

    2.2.5 导入ImportBeanDefinitionRegistrar

    以编程式向IOC容器中注册bean对象

    1.声明服务员类

    public class Waiter {
    }
    

    2.编写ImportBeanDefinitionRegistrar的实现类

    public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {
    
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            registry.registerBeanDefinition("waiter222", new RootBeanDefinition(Waiter.class));
        }
        
    }
    

    第一个参数是Bean的名称(即ID)
    第二个参数传入的RootBeanDefinition要指定Bean的字节码
    这种方式相当于向IOC容器注册了一个普通的单实例bean(最终效果与组件扫描、@Bean注解的效果相同)

    3.在@EnableTavern注解中添加WaiterRegistrar

    @Documented
    @Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
    @Target(ElementType.TYPE) // 该注解只能标注到类上
    @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
    public @interface EnableTavern {
    
    }
    

    4.测试运行

    public class TavernApplication {
    
        public static void main(String[] args) {
            AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
            Map<String, Waiter> waiters = ctx.getBeansOfType(Waiter.class);
            waiters.forEach((name, waiter) -> System.out.println(name));
            System.out.println("=======");
            Map<String, WaiterRegistrar> waiterRegistrars = ctx.getBeansOfType(WaiterRegistrar.class);
            waiterRegistrars.forEach((name, waiterRegistrar) -> System.out.println(name));
            System.out.println("=======");
        }
    
    }
    

    结果显示:服务员对象成功注册,WaiterRegistrar不会注册。

    2.2.6 扩展:DeferredImportSelector

    ImportSelector的子接口DeferredImportSelector,类似于ImportSelector,但执行时机比ImportSelector晚。
    ImportSelector:在注解配置类的解析期间,此时配置类中的Bean方法还没有被解析
    DeferredImportSelector:在注解配置类的解析完成之后
    目的:配合条件装配(后面再深入)

    1.编写WaiterDeferredImportSelector类

    public class WaiterDeferredImportSelector implements DeferredImportSelector {
    
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            System.out.println("DeferredImportSelector执行了...");
            return new String[] {Waiter.class.getName()};
        }
        
    }
    

    2.ImportSelector和ImportBeanDefinitionRegistrar也加上执行提示语

    public class BarImportSelector implements ImportSelector {
    
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            System.out.println("ImportSelector执行了...");
            return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
        }
    
    }
    
    public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {
    
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            System.out.println("ImportBeanDefinitionRegistrar执行了...");
            registry.registerBeanDefinition("waiter222", new RootBeanDefinition(Waiter.class));
        }
    
    }
    

    3.在@EnableTavern注解中添加WaiterDeferredImportSelector

    @Documented
    @Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
    @Target(ElementType.TYPE) // 该注解只能标注到类上
    @Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class, WaiterDeferredImportSelector.class})
    public @interface EnableTavern {
    
    }
    

    4.运行测试


    DeferredImportSelector的运行时机比ImportSelector晚,但比ImportBeanDefinitionRegistrar早(这样设计的原理放到后面)。
    另外,DeferredImportSelector还有分组的概念(DeferredImportSelector有一个方法getImportGroup),可以对不同的DeferredImportSelector加以区分(SpringBoot使用非常少,知道即可)。
    SpringBoot源码解读与原理分析(合集)

    相关文章

      网友评论

        本文标题:SpringBoot源码解读与原理分析(二)组件装配

        本文链接:https://www.haomeiwen.com/subject/qtzyndtx.html