spring-1-控制反转IOC、创建bean的方式、创建bean的过程

参考:

SpringBean生成流程详解 —— 由浅入深(附超精细流程图)

【一篇搞懂】 bean的生命周期详解

1.控制反转IOC

IOC(Inversion of Control,控制反转)

控制反转是一种设计原则,它将对象的创建和管理责任从应用代码中移交给容器。

在Spring中,IOC容器负责管理应用中的所有对象,包括它们的生命周期和相互之间的依赖关系。

IOC的主要目的是为了减少代码之间的耦合,使代码更加模块化和可测试。

这里的控制反转,控制的是谁,反转又是反转到哪里了?

  • 控制:对各种对象的控制
  • 反转:将控制权反转给Spring容器

嗯,就是将对象的管理交给Spring容器,不用你自己来 new Xx();

好,我们也先别管它是怎么创建对象的了,我们思考一个问题。

我们类中,是不是经常会存在。

public class Xx{
    private MyClass myclass;
}

现在问题就来了,既然Spring容器要帮我们创建这个Xx对象,那么其中依赖的其他对象,例如此处的MyClass怎么办?

这就又谈到了我们的依赖注入(Dependency Injection, DI)

依赖注入是指将对象的依赖(即它所需要的对象)在外部注入,而不是由对象自己创建或查找这些依赖。

你看,依赖注入只是帮助我们构建这个类,其根本目的还是为了实现咱们的控制反转。

好,在开始之前,我们先来大致了解几个点,留个印象。

  • IOC创建对象:通过反射完成
  • 依赖注入的几种方式
    • 构造器注入:通过构造函数来注入依赖对象。
    • Setter方法注入:通过Setter方法来注入依赖对象。
    • 接口注入:通过接口来注入依赖对象(在Spring中不常用)。

后续的内容,基于SpringBoot程序演示。

2.@SpringBootApplication注解

本文中<spring-boot.version>2.6.13</spring-boot.version>

上面已经提到了,IOC容器是用来管理咱们的对象的,Bean就是Spring IoC容器管理的一个对象,称呼而已。

好,在此之前,我们先来了解下,SpringBoot默认加载哪些bean。

回到梦开始的地方。

image-20240610120615736

一切来自于咱们的这个@SpringBootApplication注解。

image-20240610115354154

前面4个,是咱们java自带的元注解。

image-20240610120049470

注解 作用
@Target({ElementType.TYPE}) 表明可以应用于类、接口或枚举类型
@Retention(RetentionPolicy.RUNTIME) 表明在运行时可用
@Documented 表明使用的元素应被 javadoc 或类似工具记录
@Inherited 表明可以被子类继承

后面3个,是咱们Spring的。

注解 作用
@SpringBootConfiguration 这是一个特殊的 @Configuration 注解,用于标记一个类作为 Spring 配置类。
它继承自 @Configuration,表明该类可以作为 Spring IoC 容器的配置类。
@EnableAutoConfiguration 这个注解启用 Spring Boot 的自动配置机制。
Spring Boot 会根据类路径中的依赖、定义的 Bean 和各种属性设置自动配置 Spring 应用程序的上下文。
它通过读取类路径中的 META-INF/spring.factories 文件,查找可以自动配置的类。
@ComponentScan 这个注解启用组件扫描,默认扫描 @SpringBootApplication 所在包及其子包中的所有组件(如带有 @Component、@Service、@Repository 和 @Controller`的类)。
excludeFilters属性用于排除特定类型的组件。例如:
- TypeExcludeFilter.class:一个自定义过滤器类型,用于排除特定类型的组件。
- AutoConfigurationExcludeFilter.class:用于排除自动配置类。

综上,使用了 @SpringBootApplication 注解:

  • ZaApplication类被标记为一个配置类,可用于定义 Bean。

  • Spring Boot 的自动配置机制被启用:Spring Boot 会根据类路径中的依赖自动配置应用程序上下文。

  • Spring Boot 会自动扫描cn.yang37.za包及其子包中的组件,并将它们注册为 Spring Bean。

3.如何查看SpringBoot中的Bean

3.1 Actuator

注意啊,这个东西权限很大,不懂的话,你别在生产环境乱用。

这玩意就是个Spring自带的监控工具,用起来很简单。

3.1.1 导入pom

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

3.1.2 声明配置

这里就是声明暴露的端点,包含一个beans。

server:
  port: 9595
# actuator相关
management:
  endpoints:
    web:
      exposure:
        include: beans

3.1.3 启动后访问

启动后访问web,可以看到相关信息。

http://localhost:9595/actuator/beans

image-20240610125156296

3.2 ApplicationContext

这个就比较简单了,直接注入了使用就行。

例如我注册一个bean,类型是MyConfig,名字是“6666”。

package cn.yang37.za.config;

import org.springframework.stereotype.Component;

@Component("6666")
public class MyConfig {

}

然后,在测试类中获取它。

package cn.yang37.za.controller;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

import javax.annotation.Resource;

@Slf4j
@SpringBootTest
class BeanControllerTest {

    @Resource
    private ApplicationContext applicationContext;

    @Test
    void name() {
        Object myConfig = applicationContext.getBean("6666");
        log.info("info : {}", myConfig);
    }
}

可以看到相关信息。

image-20240610125652769

image-20240610125942078

4.如何创建Bean

4.1 @Componet + @ComponentScan

4.1.1 @Componet

Bean命名规则

  • 类名的首字母小写:Spring会将类名的首字母小写,其他部分保持不变,作为默认的Bean名称。比如此处的beanName是myConfig。

  • 保留原有大小写:如果类名的首字母和第二个字母都是大写(例如URLConverter),Spring会直接使用类名URLConverter作为Bean名称,而不会改变首字母的大小写。

@Componet作用于类,最简单的方式,就是直接用上咱们的@Componet注解,标记这个类要放到容器中去。

因为是一个类,所以其中肯定有各种方法,也可能有各种具体的@Bean。所以,也含义这个类的Bean是一个管理Bean的Bean,即容器管理bean

@Component
// 也可以声明一个Bean名字@Component("MyConfig666")
public class MyConfig {

}

使用的时候,直接使用@Autowired或者@Resource获取即可。

@RestController
@RequestMapping("/bean")
public class BeanController {

    @Resource
    private MyConfig myConfig;


    @GetMapping("/info")
    public String info() {
        return myConfig.toString();
    }
}

image-20240610114020906

我们平时用到的@Controller等注解,都是它的子注解。

那我到底写@Component还是@Controller?要反过来说,@Controller这些注解都不合适的时候,再用@Component。

注解 作用 适用层次
@Component 标记为Spring管理的通用bean 通用组件
@Controller 标记为Spring MVC控制器,处理HTTP请求并返回视图 表现层(MVC控制器)
@Service 标记为服务层组件,表示业务逻辑 服务层
@Repository 标记为数据访问层组件,提供数据访问功能并处理数据库异常转换 数据访问层
@RestController 标记为RESTful Web服务控制器,默认返回JSON或XML格式的数据 表现层(REST控制器)
image-20240610114532047

4.1.2 @ComponentScan

该注解呢,用于扫描某个包,将所有声明了@Component注解的类,注入到Spring容器中。

哎,你肯定问,我都@Componet了?还scan个毛?@Componet不是已经可以加载进来了吗?

你忘啦,最开始说的,启动类上的@SpringBootApplication注解包含了@ComponentScan注解,但是它的默认扫描位置是:

  • 启动类(即@SpringBootApplication的类)所在的包
  • 子包

回到项目里,就是默认只扫描cn.yang37.za文件夹和其中的文件夹。

image-20240610120937492

我们在别的位置新建一个类,即便用上注解,它也是扫描不到的。

image-20240610121437434

image-20240610121425863

这个时候,咱们的@ComponentScan注解不就有用了吗?咱们直接在类中补上这个扫描的位置,它不就启动起来啦。

image-20240610121633954

啊,你别懵,十分正常,你只是平时写习惯了,不妨看下咱们类加载器的知识:Java-JVM-类加载器

idea启动的时候,自动把classpath路径给我们传了,这下面所有的类都能在JVM里面找到。

所以咱们JVM中是有这个Demo类的,在配置了@ComponentScan到这个cn.yang的包下后,就一切正常了。

image-20240610122038897

好,实际上不会这样搞对吧?咱们Java程序员向来喜欢搞各种xx包、xx类的概念,咱们根本就做不出来旁边新建个文件夹cn.yang的鬼操作对吧?

是滴,这里是在给你演示。

常见的方式是,某个组件没在你当前项目(比如此处的ZaApplication)代码里,比如某某jar包、比如跨模块,此时没加载到Spring容器中,不妨试试这个注解。

当然,正常情况下,这个jar包应该使用咱们2.5节spring.factories的方式。

反正,你发现没自动加载上,你就试试手动声明下扫描位置,不用这么纠结到底要在何时使用。

注意,@ComponentScan不要乱写在启动类上,用在启动类上会覆盖默认的扫描范围。

4.2 @Bean

Bean命名规则:默认情况下,其名字是方法名,也可以在注解中指明名字(注解的name属性)。

@Bean作用于方法,我们可以自己创建一个对象,把它放到我们的Spring容器中去。

哎,前面不是说,咱们把对象管理交给Spring去处理吗?什么玩意这么大的面子,还得我亲自创建?

先来段专业的话术。

@Bean 注解用于告诉 Spring 容器这个方法将返回一个需要注册为 Spring Bean 的对象,这样,我们可以自定义对象的创建逻辑,尤其是在对象创建需要复杂配置或依赖于其他组件时。

上例子,假设我们有个User对象,朴实无华。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private String name;

    private int age;
}

假设我创建它的时候,我要先到excel中读取下年龄数据,再调用xx接口获取下名字。

哈哈,你也知道我在乱扯。肯本原因就是,Spring哪能啥都知道啊,它也只是个框架,像上面的例子中,你还能指望Spring知道你要读取excel来加载年龄?

正如咱们自己写代码的时候,场景就是千变万化的,@Bean的用处,是告诉你,当某些复杂场景搞不定的时候,你可以自己定义bean的注册逻辑。

所以,复杂点的场景,我要yang某活到1666岁,咱们可以自己放一个想要的Bean进去。

image-20240610131455815

image-20240610131618672

又比如你在用Redis时,经常看到的StringRedisTemplate,这不就是你不满意默认的StringRedisTemplate嘛,你要搞一个给它覆盖了。

    @Bean
    public StringRedisTemplate stringRedisTemplate(@Qualifier("redissonConnectionFactory") RedisConnectionFactory connectionFactory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        stringRedisTemplate.setConnectionFactory(connectionFactory);
        stringRedisTemplate.setKeySerializer(stringRedisSerializer);
        stringRedisTemplate.setValueSerializer(stringRedisSerializer);

        stringRedisTemplate.afterPropertiesSet();
        return stringRedisTemplate;
    }

4.3 @Configuration

Bean命名规则:跟@Componen一样的。

这个玩意呢,打开里面还是@Component,适用于配置类的场景。

image-20240610132927692

所以我们可以知道,它也能注册bean,那既然有自己的名字,肯定做了些定制化的操作。

Configuration这个单词你总该认识吧?什么,这你都不认识?

image-20240610133141821

那看名字我们也能猜到了,它就是专门针对配置类的场景

配置类的话,它的关键点在哪里,在于单例,即咱们的配置项key=123,它不会成key=456,所以@Configuration针对这点做了优化。

不过呢,这个优化点咱们一般感受不到。

Spring 会对 @Configuration 类进行 CGLIB 动态代理增强,确保@Bean对应的方法只被调用一次,直接返回相同的实例(单例)。

有点抽象对不对,我们来看个例子。

@Component
public class MyConfig {

    @Bean
    public User user1() {
        return new User();
    }

    @Bean
    public User user2() {
        // 调用user1方法
        return user1();
    }
    
}

这里我们用的@Component注解,关键点在于下方的user2,它是在调用user1()方法。

此时,@Component注解返回了两个不同的对象。

image-20240610140720240

那我们改成@Configuration

@Configuration
public class MyConfig {

    @Bean
    public User user1() {
        return new User();
    }

    @Bean
    public User user2() {
        return user1();
    }

}

此时,@Component返回的对象是同一个。

image-20240610140822053

哎,有点奇怪了对吧。

注意啊,注意,这不是@Component多例@Configuration单例,是强调这两个类中的@Bean对应的方法执行时会不会被代理掉。

Bean的单例还是多例,取决于Bean它自己本身声明的是单例bean还是原型bean。

你别晕,我给你改造下,是@Component对吧,注册了一个user1的bean(默认为单例bean)。

@Component
public class MyConfig {

    @Bean
    public User user1() {
        return new User();
    }

}

因为它是默认的单例Bean,不管你怎么调用它都还是同一个。

image-20240611165300981

你改成原型Bean。

@Component
public class MyConfig {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public User user1() {
        return new User();
    }
}

image-20240611165408107

你看,生成出来的Bean是单例还是多例,取决于它自身是单例Bean还是原型Bean。

回顾一下之前的User2这个Bean。

    @Bean
    public User user2() {
        return user1();
    }

    @Bean
    public User user2() {
        return new User();
    }

上方的根本逻辑是什么,是我们主动去触发user1()这个方法了,user1方法干嘛的,给你new个User出来。

@Configuration 中所有带@Bean注解的方法都会被动态代理,调用该方法首次执行后,总该有返回值了吧?后续呢,它不会再执行,直接返回这个实例(除非你又自己主动去强调Bean的类型是原型Bean)。

@Component,注意啊,这里的user2是个原型Bean,意味着每次都会返回新的对象,触发咱们的user1()方法。

image-20240611170336291

image-20240611170410978

改成@Configuration呢,会发现,user1()方法不会多次触发。

image-20240611170429777

image-20240611170452176

哎,你肯定奇怪,那我平时也没有跑去调那个@Bean下面的方法呀。

声明就声明,我没事我乱主动调用干嘛,我自己注册两个User对象,我闲的没事我跑去用user1()方法创建啊?

这应该就是针对这个场景的防范式编码了。

  • 首先呢,咱们不乱调用,Spring源码里面可能涉及到多次调用,是为了确保@Configuration它符合自己的特性。
  • 其次呢,也是为了避免你乱来,在咱们@Configuration类中写的玩意,哪怕你自己乱用,咱们的配置对象也不会混乱。

4.4 @Import

Bean命名规则:跟@Componen一样的。

@Configuration可以将某个类标记为配置类,当配置类比较多的时候呢,我们可以不用再在每个类上写@Configuration,而是统一在一个类中借用@Import引入。

比如这样:

MyConfig2、MyConfig3都上没有注解

public class MyConfig2 {
    @Bean
    User user3(){
        return new User();
    }
}
public class MyConfig3 {
    @Bean
    User user4() {
        return new User();
    }
}

咱们在AppConfig类中引入它们两个配置类。

@Configuration
@Import({MyConfig2.class, MyConfig3.class})
public class AppConfig {

}

验证一下,两个类都能被成功导入。

image-20240611171828484

嗯,这是@Import的一个简单用法,这样我们只用在一个类中(例如AppConfig)去找都导入了哪些玩意,方便管理。

然后呢,还有个常用的用法就是动态导入配置,编写一个类来实现ImportSelector接口即可。

配置选择类

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        LinkedList<String> configList = new LinkedList<>();
        // 根据条件导入配置类
        if (true) {
            configList.add(MyConfig2.class.getName());
            configList.add(MyConfig3.class.getName());
        }

        return configList.toArray(new String[0]);
    }

}

这里我list.toArray(new String[0])是个正确的写法,返回的就是对应的那个2个长度的数组。并且是建议这样写的,你可以去自己搜索下。

在@Import中使用配置选择类

@Configuration
@Import(MyImportSelector.class)
public class AppConfig {

}

验证结果

image-20240611173026448

4.5 FactoryBean接口

FactoryBean是一个工厂Bean,可以生成指定类型的Bean实例。

实现FactoryBean接口,可以对Bean创建提供更高的灵活性和扩展性,例如动态创建不同类型的 Bean、复杂初始化逻辑、延迟初始化等场景。

好,简单来说,就是我们想要创建某个类型的Bean的时候,可以找这个FactoryBean要。

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

比如上方的User,我们想要创建该类型的bean。

重写的getObject方法中,我们描述详细的创建逻辑。

  • sex为1时,age是18。
  • sex为2时,age是20。
@NoArgsConstructor
@AllArgsConstructor
public class UserFactoryBean implements FactoryBean<User> {

    private int sex;

    @Override
    public User getObject() {
        User user = User.builder()
                .name("yang")
                .age(0)
                .build();

        if (1 == sex) {
            user.setAge(18);
        }
        if (2 == sex) {
            user.setAge(20);
        }

        return user;
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }

}

注册这个UserFactoryBean。

  • userFactoryBean1:sex为1,其中创建的user的age是18。
  • userFactoryBean2:sex为2,其中创建的user的age是20。
@Slf4j
@Configuration
public class MyConfig {

    @Bean
    public UserFactoryBean userFactoryBean1() {
        return new UserFactoryBean(1);
    }

    @Bean
    public UserFactoryBean userFactoryBean2() {
        return new UserFactoryBean(2);
    }

}

此时,咱们利用UserFactoryBean来获取User对象,并没有说去@Bean来一个user。

image-20240611190430845

哈,这里我拿的Bean名字不是userFactoryBean1吗,咱直接返回里面的User对象了?往后看。

简单来说,就是为了方便,我都开始写FactoryBean了,当然是关注于拿到实际的Bean,咱默认设计成给你直接返回对应Bean实例。

然后呢,你还可以这样用。image-20240611191112047

额,这里你可能看到我用了个&userFactoryBean1,这个&是何用意?

& 前缀在 Spring 中是专门用来获取FactoryBean实例本身的。

即通过 getBean方法获取 Bean 时,FactoryBean这个玩意,默认情况下返回的是它创建的对象。

  • 违背预期:大多数情况下,开发者希望获取的是由 FactoryBean 创建的对象,而不是 FactoryBean 本身。

    例如,当你配置了一个 UserFactoryBean,你希望得到的是 User 对象,而不是 UserFactoryBean的 实例。

  • 简化使用:通过 getBean 方法直接获取目标对象,简化了代码的使用,不需要开发者额外处理 FactoryBean 实例。

  • 避免混淆:明确区分 FactoryBean 和它创建的对象,有助于代码的可读性和可维护性。

那么在例子中,UserFactoryBean本身是不会直接注入的,默认注册的是 UserFactoryBean创建的User,为了注入 UserFactoryBean实例,得像我下面这样:

    @Autowired
    @Qualifier("&userFactoryBean1")
    private UserFactoryBean userFactoryBean1;

不用太纠结,有个印象即可,毕竟,报错了你会搜索,而且,也不是不给你拿,你加&指明下就好了。

有点懵是吧,上图。

image-20240611193658510

4.6 BeanDefinitionRegistryPostProcessor接口

BeanDefinitionRegistryPostProcessor与上面FactoryBean接口类似,实现这个接口,也能创建出Bean。

@Slf4j
@Configuration
public class UserBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        log.info("=====> UserBeanDefinitionRegistryPostProcessor");
        // 创建User类的Bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class);
        beanDefinitionBuilder.addPropertyValue("name", "yang");
        beanDefinitionBuilder.addPropertyValue("age", 166);

        // 注册User类的Bean定义
        beanDefinitionRegistry.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        // 这里可以留空,或者用于进一步定制 BeanFactory
    }
}

image-20240611194935762

4.7 spring.factories

这个呢,主要用于三方包的场景。

前面提到了,咱们默认扫描的是启动类和其子包,那我写给别人的stater,我还发个聊天消息给别人说,你来配置下扫描我的xx包?

spring.factories呢无非就是一个固定的文件,咱们写在里面的类,会被自动注册到容器中。

原理呢是基于Java的SPI机制的。

它的位置固定为:

META-INF/spring.factories

好,不妨演示一下。

  • 旧项目cn.yang37
  • 新项目com.yang37

4.7.1 旧项目

我们直接把当前项目清空,你看,啥都没有,只有个简单的Controller和启动类。

image-20240611201444843

记录下当前项目的坐标。

    <groupId>cn.yang37</groupId>
    <artifactId>yang-spring-za</artifactId>
    <version>0.0.1-SNAPSHOT</version>

image-20240611201547931

4.7.2 新项目

然后呢,直接新开一个项目,用不同的包名总可以了吧,新项目用com.yang37-2,这差别够大了吧。

    <groupId>com.yang37-2</groupId>
    <artifactId>yang-spring-za2</artifactId>
    <version>0.0.1-SNAPSHOT</version>

它里面啥也没有,只是导入了spring-boot-starter,代码呢写了两个实体类和配置类,启动类都没有。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

image-20240611201812661

两个实体类,朴实无华,也没用spring的注解。

package com.yang37.za.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User2 {

    private String name;

    private int age;
}
package com.yang37.za.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User3 {
    private String name;

    private int age;

}

然后呢,配置类中,我们把它俩注册为Bean。

package com.yang37.za.config;

import com.yang37.za.entity.User2;
import com.yang37.za.entity.User3;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfig {

    @Bean
    public User2 user2() {
        return User2.builder()
                .name("user2")
                .age(30)
                .build();
    }

    @Bean
    public User3 user3() {
        return User3.builder()
                .name("user3")
                .age(30)
                .build();
    }

}

接着我们在META-INF\spring.factories声明需要加载UserConfig。

# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.yang37.za.config.UserConfig

image-20240611202510674

最后我们将这个项目install。

image-20240611202551996

4.7.3 验证

现在,我们在旧工程中导入打包好的新项目。

        <dependency>
            <groupId>com.yang37-2</groupId>
            <artifactId>yang-spring-za2</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

image-20240611202718928

你看,已经加载进来了。

image-20240611202818037

停,先回顾下。

  • 旧项目cn.yang37:啥也没有
  • 新项目com.yang37:啊,是个外部的玩意,里面记录了要加载哪些bean。

你看,我们在当前项目中,引入了这个带有spring.factories的三方包,我们旧项目自身啥也没有对吧?

验证下,你看,一切都是如期而至。

image-20240611203131426


哎,前面不是讲了,咱们用scan是不是也可以,我们把com.yang37里面的spring.factories删除了重新打包。

没了之后,我们不@ComponentScan直接报错。

image-20240611203448302

加下@ComponentScan,是不是正常啦。

image-20240611203606098

好咯,回顾下,条条大路通罗马,无非就是适用的场景不一样而已。

4.7.4 总结

上面的例子只是演示了一个类似于加载自定义配置类的玩意,实际上咱们有很多场景可以用到。

spring.factories 文件中常见配置项的总结。

配置项类型 说明
org.springframework.boot.autoconfigure.EnableAutoConfiguration 用于指定自动配置类。
org.springframework.context.ApplicationContextInitializer 用于指定初始化 Spring 上下文的类。
org.springframework.context.ApplicationListener 用于指定应用程序事件监听器。
org.springframework.boot.env.EnvironmentPostProcessor 用于在 Spring Boot 应用程序环境准备好后进行处理的类。
org.springframework.boot.SpringApplicationRunListener 用于在 SpringApplication 运行时的监听器。

demo:

# META-INF/spring.factories

# Auto Configuration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.yang37.za.config.UserConfig,\
  com.yang37.za.config.OtherConfig

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
  com.yang37.za.config.SomeInitializer,\
  com.yang37.za.config.AnotherInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
  com.yang37.za.config.SomeListener,\
  com.yang37.za.config.AnotherListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.yang37.za.config.SomeEnvironmentPostProcessor,\
  com.yang37.za.config.AnotherEnvironmentPostProcessor

# Spring Application Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
  com.yang37.za.config.SomeRunListener,\
  com.yang37.za.config.AnotherRunListener

5.Bean的创建过程

详细的过程肯定不止如此,此处只是介绍一些主要步骤。

我们先简单描述下,创建Bean的基本思路。

  • 创建前准备

  • 加载配置文件:Spring会加载应用程序的配置文件或注解。

  • 创建并配置环境:包括设置环境变量和属性源等。

  • 初始化BeanFactory:准备好BeanDefinition和BeanFactory等。

  • 创建实例

  • BeanFactory根据BeanDefinition创建Bean,此时bean是个空的实例化对象,还没有被填充进来需要的属性。

  • 填充Bean属性(依赖注入)

  • 根据需要的属性,完成注入工作,此时,对象中具备咱们注入的属性了。

  • 初始化(容器缓存)

  • 执行一些额外工作,例如。

    • 应用BeanPostProcessor:在Bean初始化前后应用自定义的BeanPostProcessor。
      • 初始化回调:调用Bean的初始化方法,如@PostConstruct注解的方法或实现的InitializingBean接口的afterPropertiesSet方法等。
      • 注册Bean:将创建好的Bean实例注册到BeanFactory中,供后续使用。

啊,华而无实,实际上,咱们主体思路就是这么简单。

只不过既然能让Java程序员变成Spring程序员,那必然得十分强大,咱不得给每个步骤加上无数个前后执行点啊,每个节点能让你随心所欲修改。

这些给你提供修改点的玩意,被称为PostProcessor(后置处理器)

27100708_62b910cc8c44d1365

现在来个实例,以咱们之前的项目为例。我把多余的都删除掉了,甚至web依赖都移除掉了。

只用到了:

  • spring-boot-starter:核心依赖
  • spring-boot-starter-log4j2:日志
  • spring-boot-starter-test:测试用
  • fastjson
  • lombok
    <dependencies>
        <!-- spring-boot-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- spring-boot-starter-log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.14</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

嗯,不要忘了,咱们的SpringBoot只是在web后端的场景比较多,不是非得web。

项目呢,只保留一个启动类,一个配置类。

image-20240612192440023

image-20240612192419208

配置文件也不需要吧,咱们都不是web,不需要配置端口。

启动一手,那必然能启动哈。

哈哈,不过,启动之后就自动停止了,为啥,还是因为咱们不是web项目,无需一直运行。

image-20240612192607098

5.1 BeanFactory

BeanFactory是Spring框架中最基础的容器接口,负责管理Bean的生命周期,包括创建、配置和销毁。

它通过延迟加载的方式管理Bean,即在第一次访问Bean时才实例化它。

BeanFactory主要的实现类(包括抽象类):

  • AbstractBeanFactory:抽象Bean工厂,绝大部分的实现类,都是继承于它;
  • DefaultListableBeanFactory:Spring默认的工厂类;
  • XmlBeanFactory:前期使用XML配置用的比较多的时候用的Bean工厂;
  • AbstractXmlApplicationContext:抽象应用容器上下文对象;
  • ClassPathXmlApplicationContext:从 xml 的配置文件中获取 bean 并且管理它们;

image-20240613220553027

SpringBoot程序启动时候呢,就会创建一个默认的DefaultListableBeanFactory

image-20240613223644685

主要功能

  1. Bean定义管理:能够注册、获取、移除和遍历Bean定义。
  2. Bean实例化:支持创建单例和原型Bean实例。
  3. 依赖注入:支持自动装配和手动装配Bean的依赖。
  4. 作用域管理:支持单例、原型以及自定义作用域。
  5. 事件传播:支持Bean生命周期事件的传播和处理。
  6. 自动装配候选者解析:确定哪些Bean可以自动装配。
  7. 配置冻结:可以在初始化完成后冻结配置,防止进一步的Bean定义修改。
  8. 别名管理:支持为Bean定义注册别名。

主要字段:

  • beanDefinitionMap: 存储所有Bean定义的映射。

  • beanDefinitionNames: 存储所有Bean定义的名称列表。

  • resolvableDependencies: 存储可解析的依赖项,例如类型转换器等。

  • singletonObjects: 第一级缓存,存储单例Bean实例的缓存。

  • earlySingletonObjects:第二级缓存,存储早期暴露的单例Bean实例,主要用于解决循环依赖。

  • singletonFactories:第三级缓存,存储能够创建单例Bean实例的工厂。

5.2 BeanDefinition(bd)

BeanDefinition描述了一个Bean的定义信息,包括Bean的类名、作用域、是否懒加载、依赖关系、初始化方法、销毁方法等。

在Bean未实例化之前,咱们的Bean是以BeanDefinition的形式存在的,就是存放一些Bean的定义,然后通过BeanFactory创建。

好,我们从启动类开始看下。

image-20240612201303838

run方法呢,进去会走到这里,别的先不管,我们注意这几处。

前面不是贴了张图,最开始创建了默认的BeanFactory。

image-20240613225004507

然后,我们直接给BeanFactory中的beanDefinitionMap添加监视,关注它的数量变化,看这个map的数量啥时候变多了,变多了,肯定是扫描到需要的BeanDefinition了呗。image-20240613225206715

或者,你像我这样,把它置顶,反正我们的主要目的是观察在哪个位置数量变化了。

image-20240613225610167

然后,你应该能观察到,在这这两行之间,这个map的size变化了,像我这里就是从6变化到47了。

            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);

image-20240613230126678

好,那我们现在知道经过prepareContext()之后,这个map变化了,关键是怎么找到具体的位置?

进去具体的prepareContext()方法,然后逐行执行下,现在我们发现是在this.refresh(context);这里变化的。

image-20240613230418096

所以你该把断点往后挪,前面的可以取消了。

image-20240613230548284

让你改断点的意思是,逐步精确位置,哪怕中间出问题了,我们也能快速还原现场,你看我现在就可以直接重启,立马就到这里了,而且我知道接下来它会变化了。

image-20240613230658594

这才开始,坚持住哥,别慌。

跟着走几个方法,你会发现进了个大的方法,按刚才的逻辑,缩小断点,这里我先直接打到方法首部和尾部。

为啥我这样搞,首先,这里右侧的监视器已经丢失了。而且,我这个方法执行完,肯定值会变化。

我们先首位打断点,二分找位置。

image-20240613230846752

image-20240613231032766

你看,到这里,我们知道在这之间发生了变化。二分断点法,我直接把尾部的提前到中间。

image-20240613231243115

image-20240613231345860

你看,我们范围已经缩小很多了。

image-20240613231426981

下一步是啥,下一步我肯定是去除掉最开头的那个断点。

你别慌,现在范围缩小了,你把其余断点关闭下,不麻烦,哪怕你重启,那不是立马还原现场?

image-20240613231511646

你看,又定位到一个位置了,在这里变化了。

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

image-20240613231640270

思路是什么,往里面进。

image-20240613231815143

image-20240613231917341

继续继续。

image-20240613232216681

定位到这里变了7,是不是得往方法里面进。

image-20240613232302316

再进。

image-20240613232441608

image-20240613232552234

你看到这个玩意,是不是感觉这是精髓的代码,就在这里变成7了。

image-20240613232844827

        private void register(BeanDefinitionRegistry registry) {
            if (!registry.containsBeanDefinition("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory")) {
                BeanDefinition definition = BeanDefinitionBuilder.rootBeanDefinition(SharedMetadataReaderFactoryBean.class, SharedMetadataReaderFactoryBean::new).getBeanDefinition();
                registry.registerBeanDefinition("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory", definition);
            }

        }

org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory这么长这么长的名字啊?这是啥玩意BeanDefinition啊?

我也不懂,嗯,我咋可能懂啊?gpt问下。

又tm来抽象话术。

image-20240613233153266

咱不懂,但是咱大概能猜到,对吧,这里不就是看if有没有嘛,没有我给你加一个,看起来似乎是框架自身用的。

所以,这里不是我们要的那个关键点,我们的注意力换成,这个size的7咋变46了?

按照之前的思路,先找到7,逐个往后放断点,轻松的调整一下我们的断点,定位出一个区域。

image-20240613233509720

image-20240613233758976

嗯,所以,搞半天,又来到这里。

			invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
			currentRegistryProcessors.clear();

一样的操作,进invokeBeanDefinitionRegistryPostProcessors,继续缩小范围。

image-20240613233928849

image-20240613234039905

然后呢,中间经过一番小小的摸索,我们就能找到这里。

org/springframework/context/annotation/ClassPathBeanDefinitionScanner.javabeanDefinitions.add(definitionHolder);

image-20240614001420443

好,这里你注意我左侧config文件夹下的3个aware类,在这里被扫描到加进去BeanDefinition了。

					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);

那这里的definitionHolder和this.registry都是啥。

  • definitionHolder看不懂,反正我们知道它持有BeanDefinition。
  • this.registry就是我们的BeanFactory嘛

image-20240614000835643

那其实多点两步也能看到,最后就是把我们的BeanDefinition根据名字来放到BeanFactory中去。

image-20240614000940612

现在我们来回顾下,大概是干嘛了。

  • SpringBoot启动的时候呢,会扫描到咱们需要注入的类。
  • 把这些类的BeanDefinition信息,放入到BeanFactory。

那我想知道咋就扫出来了,是不是该看这个for。

image-20240614001738334

image-20240614002345092

image-20240614002732516

image-20240614002920845

这个方法就是在判断咱们的一个类是否是候选组件。

候选组件是指那些可以被Spring容器管理的类,比如带有特定注解的类(如 @Component@Service@Repository 等)。

5.3 BeanDefinitionRegistry

BeanDefinitionRegistry 是Spring中的一个接口,用于注册和管理Bean定义。

它提供了一些方法来动态地向Spring容器注册新的Bean定义或移除现有的Bean定义。

常用的实现类是 DefaultListableBeanFactory,它既是一个 BeanFactory,也是一个 BeanDefinitionRegistry

image-20240615141219857

我们先微调下代码,我把注册的类改成了MyComponent,就一个类。

image-20240615141258436

好,上一节我们在注册BeanDefinition的时候,不是找到了一个方法吗?

org/springframework/beans/factory/support/BeanDefinitionReaderUtils.javaregisterBeanDefinition

image-20240615140529803

这里就是在注册BeanDefinition,但是我们肯定会疑问,这里的registry是啥?看一眼呗。

image-20240615140635351

还记得DefaultListableBeanFactory吗,它不就是我们在开始提到的,默认BeanFactory。

image-20240615140752403

所以,注册BeanDefinition,就是在往自身的map里面put值。

image-20240615141041339

嗯,最后,来总结下,BeanDefinitionRegistry无非就是这个接口。

实现了这个接口,它必然得实现registerBeanDefinition、removeBeanDefinition、getBeanDefinition这个些方法。

那我们看到一个类实现它,无非就是说明这个类能管理BeanDefinition。

有这个印象就够了。

然后,截止到目前,我们知道了什么?

我们已经知道,咋们SpringBoot程序在启动后。

  • 会创建BeanFactory
  • 然后会扫描需要注册的Bean
  • 最后会加载它们的BeanDefinition到beanFactory自身的beanDefinitionMap中去

之后该干嘛,是不是该创建了?

那我们之前有了解到,像我们的MyComponent这种就是放到BeanFactory的单例池中去。

创建,我们是不是该和之前的思路一样,先在最外层关注下singletonObjects的数量,再看看在哪里发生变化了?

image-20240615142501130

所以是不是很容易定位到一切的尽在refreshContext这个方法中?然后复刻之前的思路,逐步缩小范围。

image-20240615142607426

是不是很容易的就能找到我们这个myComponent跟org/springframework/beans/factory/support/DefaultListableBeanFactory.java的preInstantiateSingletons()方法有关?

image-20240615143401203

然后进到实际的doGetBean方法中,我们的关注点是不是应该发生转变吗,看Object beanInstance;这个玩意什么时候有值的?

image-20240615143635203

5.3 BeanWarpper(bw)

BeanWrapper 是Spring框架中用于封装Bean对象的工具,提供了对Bean属性的设置和获取方法。它通过反射机制直接操作Bean的属性,常用于数据绑定和依赖注入。

5.7 PostProcessor

Spring中的后置处理器分为两大类。

  • 针对Bean工厂的:BeanFactoryPostProcessor

  • 针对Bean的:BeanPostProcessor

5.3.1 BeanPostProcessor

BeanPostProcessor,通常译为Bean后置处理器,它是Spring 框架中的一个接口,用于在 Spring 容器初始化 bean 前后进行额外处理。

image-20240612193605697

你看,里面给了你2个方法,Before和After。

很好猜对不对,就是创建Bean的前后会调用这个方法。

好咯,也不用看资料,无脑重写一手。

@Slf4j
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        log.warn("postProcess ---------------------> Before Initialization");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        log.warn("postProcess ---------------------> After Initialization");
        return bean;
    }
}

启动看看,哇靠,你看,是不是跟我们想的一样,前后执行一手。

image-20240612193950024

哎,那我们项目里不是有个MyConfig吗,它会触发吧?

@Configuration
public class MyConfig {

}

形参这么简单,岂不是很好用,加一行666的输出。

image-20240612194330594

5.3.2 BeanFactoryPostProcessor

如前面所言,我们的Bean工厂中会尝试根据BeanDefinition加载Bean。

同样,我们可以重写BeanFactoryPostProcessor来做一些操作。

@Slf4j
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

        log.warn("postProcessBeanFactory ---------------------");
    }
}

image-20240613101222530

5.3 Aware

Aware接口主要用于让Bean感知到Spring容器的存在,并获取Spring容器中的一些资源或信息。

image-20240613220120452

很抽象对不,还感知?还获取资源?怎么这么恶心啊这些名词,tmd我根本搞不懂啊。

不管不管,我先来个类实现下。

@Slf4j
@Component
public class MyApplicationContextAware implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        log.warn(applicationContext.getClass().getName());
        log.warn("MyApplicationContextAware..................");
    }
}

原来实现了这个方法就能把applicationContext给你,再来一个。

@Slf4j
@Component
public class MyBeanFactoryAware implements BeanFactoryAware {
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        log.error(beanFactory.getClass().getName());
        log.error("MyBeanFactoryAware..................");
    }
}

嗯,beanFactory也给我们了。

启动一手,可以看到这两个方法确实被执行了。

image-20240613221905421

好了,现在有点印象了对不对,大概就是XxAware,就能给你提供Xx给你让你访问。嗯,只能访问不能改,我给你试了。

image-20240613222232207

5.4 总结

6.Bean的生命周期

27100708_62b910cc9f28f73282

posted @ 2024-06-11 20:44  羊37  阅读(15)  评论(1编辑  收藏  举报