spring-1-控制反转IOC、创建bean的方式、创建bean的过程
参考:
SpringBean生成流程详解 —— 由浅入深(附超精细流程图)
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。
回到梦开始的地方。
一切来自于咱们的这个@SpringBootApplication
注解。
前面4个,是咱们java自带的元注解。
注解 | 作用 |
---|---|
@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
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);
}
}
可以看到相关信息。
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();
}
}
我们平时用到的@Controller
等注解,都是它的子注解。
那我到底写@Component还是@Controller?要反过来说,@Controller这些注解都不合适的时候,再用@Component。
注解 | 作用 | 适用层次 |
---|---|---|
@Component | 标记为Spring管理的通用bean | 通用组件 |
@Controller | 标记为Spring MVC控制器,处理HTTP请求并返回视图 | 表现层(MVC控制器) |
@Service | 标记为服务层组件,表示业务逻辑 | 服务层 |
@Repository | 标记为数据访问层组件,提供数据访问功能并处理数据库异常转换 | 数据访问层 |
@RestController | 标记为RESTful Web服务控制器,默认返回JSON或XML格式的数据 | 表现层(REST控制器) |
4.1.2 @ComponentScan
该注解呢,用于扫描某个包,将所有声明了@Component
注解的类,注入到Spring容器中。
哎,你肯定问,我都@Componet
了?还scan个毛?@Componet不是已经可以加载进来了吗?
你忘啦,最开始说的,启动类上的@SpringBootApplication
注解包含了@ComponentScan
注解,但是它的默认扫描位置是:
- 启动类(即
@SpringBootApplication
的类)所在的包 - 子包
回到项目里,就是默认只扫描cn.yang37.za
文件夹和其中的文件夹。
我们在别的位置新建一个类,即便用上注解,它也是扫描不到的。
这个时候,咱们的@ComponentScan注解不就有用了吗?咱们直接在类中补上这个扫描的位置,它不就启动起来啦。
啊,你别懵,十分正常,你只是平时写习惯了,不妨看下咱们类加载器的知识:Java-JVM-类加载器
idea启动的时候,自动把classpath路径给我们传了,这下面所有的类都能在JVM里面找到。
所以咱们JVM中是有这个Demo类的,在配置了@ComponentScan到这个cn.yang的包下后,就一切正常了。
好,实际上不会这样搞对吧?咱们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进去。
又比如你在用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
,适用于配置类的场景。
所以我们可以知道,它也能注册bean,那既然有自己的名字,肯定做了些定制化的操作。
Configuration这个单词你总该认识吧?什么,这你都不认识?
那看名字我们也能猜到了,它就是专门针对配置类的场景。
配置类的话,它的关键点在哪里,在于单例,即咱们的配置项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
注解返回了两个不同的对象。
那我们改成@Configuration
。
@Configuration
public class MyConfig {
@Bean
public User user1() {
return new User();
}
@Bean
public User user2() {
return user1();
}
}
此时,@Component
返回的对象是同一个。
哎,有点奇怪了对吧。
注意啊,注意,这不是说@Component
多例@Configuration
单例,是强调这两个类中的@Bean对应的方法执行时会不会被代理掉。
Bean的单例还是多例,取决于Bean它自己本身声明的是单例bean还是原型bean。
你别晕,我给你改造下,是@Component
对吧,注册了一个user1的bean(默认为单例bean)。
@Component
public class MyConfig {
@Bean
public User user1() {
return new User();
}
}
因为它是默认的单例Bean,不管你怎么调用它都还是同一个。
你改成原型Bean。
@Component
public class MyConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public User user1() {
return new User();
}
}
你看,生成出来的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()方法。
改成@Configuration
呢,会发现,user1()方法不会多次触发。
哎,你肯定奇怪,那我平时也没有跑去调那个@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 {
}
验证一下,两个类都能被成功导入。
嗯,这是@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 {
}
验证结果
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。
哈,这里我拿的Bean名字不是userFactoryBean1吗,咱直接返回里面的User对象了?往后看。
简单来说,就是为了方便,我都开始写FactoryBean了,当然是关注于拿到实际的Bean,咱默认设计成给你直接返回对应Bean实例。
然后呢,你还可以这样用。
额,这里你可能看到我用了个&userFactoryBean1
,这个&
是何用意?
&
前缀在 Spring 中是专门用来获取FactoryBean实例本身的。
即通过 getBean方法获取 Bean 时,FactoryBean这个玩意,默认情况下返回的是它创建的对象。
-
违背预期:大多数情况下,开发者希望获取的是由
FactoryBean
创建的对象,而不是FactoryBean
本身。例如,当你配置了一个
UserFactoryBean
,你希望得到的是User
对象,而不是UserFactoryBean
的 实例。 -
简化使用:通过
getBean
方法直接获取目标对象,简化了代码的使用,不需要开发者额外处理FactoryBean
实例。 -
避免混淆:明确区分
FactoryBean
和它创建的对象,有助于代码的可读性和可维护性。
那么在例子中,UserFactoryBean本身是不会直接注入的,默认注册的是 UserFactoryBean创建的User,为了注入 UserFactoryBean实例,得像我下面这样:
@Autowired
@Qualifier("&userFactoryBean1")
private UserFactoryBean userFactoryBean1;
不用太纠结,有个印象即可,毕竟,报错了你会搜索,而且,也不是不给你拿,你加&
指明下就好了。
有点懵是吧,上图。
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
}
}
4.7 spring.factories
这个呢,主要用于三方包的场景。
前面提到了,咱们默认扫描的是启动类和其子包,那我写给别人的stater,我还发个聊天消息给别人说,你来配置下扫描我的xx包?
spring.factories
呢无非就是一个固定的文件,咱们写在里面的类,会被自动注册到容器中。
原理呢是基于Java的SPI机制的。
它的位置固定为:
META-INF/spring.factories
好,不妨演示一下。
- 旧项目cn.yang37
- 新项目com.yang37
4.7.1 旧项目
我们直接把当前项目清空,你看,啥都没有,只有个简单的Controller和启动类。
记录下当前项目的坐标。
<groupId>cn.yang37</groupId>
<artifactId>yang-spring-za</artifactId>
<version>0.0.1-SNAPSHOT</version>
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>
两个实体类,朴实无华,也没用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
最后我们将这个项目install。
4.7.3 验证
现在,我们在旧工程中导入打包好的新项目。
<dependency>
<groupId>com.yang37-2</groupId>
<artifactId>yang-spring-za2</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
你看,已经加载进来了。
停,先回顾下。
- 旧项目cn.yang37:啥也没有
- 新项目com.yang37:啊,是个外部的玩意,里面记录了要加载哪些bean。
你看,我们在当前项目中,引入了这个带有spring.factories的三方包,我们旧项目自身啥也没有对吧?
验证下,你看,一切都是如期而至。
哎,前面不是讲了,咱们用scan是不是也可以,我们把com.yang37里面的spring.factories删除了重新打包。
没了之后,我们不@ComponentScan直接报错。
加下@ComponentScan,是不是正常啦。
好咯,回顾下,条条大路通罗马,无非就是适用的场景不一样而已。
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中,供后续使用。
- 初始化回调:调用Bean的初始化方法,如
- 应用BeanPostProcessor:在Bean初始化前后应用自定义的BeanPostProcessor。
啊,华而无实,实际上,咱们主体思路就是这么简单。
只不过既然能让Java程序员变成Spring程序员,那必然得十分强大,咱不得给每个步骤加上无数个前后执行点啊,每个节点能让你随心所欲修改。
这些给你提供修改点的玩意,被称为PostProcessor(后置处理器)。
现在来个实例,以咱们之前的项目为例。我把多余的都删除掉了,甚至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。
项目呢,只保留一个启动类,一个配置类。
配置文件也不需要吧,咱们都不是web,不需要配置端口。
启动一手,那必然能启动哈。
哈哈,不过,启动之后就自动停止了,为啥,还是因为咱们不是web项目,无需一直运行。
5.1 BeanFactory
BeanFactory是Spring框架中最基础的容器接口,负责管理Bean的生命周期,包括创建、配置和销毁。
它通过延迟加载的方式管理Bean,即在第一次访问Bean时才实例化它。
BeanFactory主要的实现类(包括抽象类):
- AbstractBeanFactory:抽象Bean工厂,绝大部分的实现类,都是继承于它;
- DefaultListableBeanFactory:Spring默认的工厂类;
- XmlBeanFactory:前期使用XML配置用的比较多的时候用的Bean工厂;
- AbstractXmlApplicationContext:抽象应用容器上下文对象;
- ClassPathXmlApplicationContext:从 xml 的配置文件中获取 bean 并且管理它们;
SpringBoot程序启动时候呢,就会创建一个默认的DefaultListableBeanFactory
。
主要功能:
- Bean定义管理:能够注册、获取、移除和遍历Bean定义。
- Bean实例化:支持创建单例和原型Bean实例。
- 依赖注入:支持自动装配和手动装配Bean的依赖。
- 作用域管理:支持单例、原型以及自定义作用域。
- 事件传播:支持Bean生命周期事件的传播和处理。
- 自动装配候选者解析:确定哪些Bean可以自动装配。
- 配置冻结:可以在初始化完成后冻结配置,防止进一步的Bean定义修改。
- 别名管理:支持为Bean定义注册别名。
主要字段:
-
beanDefinitionMap: 存储所有Bean定义的映射。
-
beanDefinitionNames: 存储所有Bean定义的名称列表。
-
resolvableDependencies: 存储可解析的依赖项,例如类型转换器等。
-
singletonObjects: 第一级缓存,存储单例Bean实例的缓存。
-
earlySingletonObjects:第二级缓存,存储早期暴露的单例Bean实例,主要用于解决循环依赖。
-
singletonFactories:第三级缓存,存储能够创建单例Bean实例的工厂。
5.2 BeanDefinition(bd)
BeanDefinition描述了一个Bean的定义信息,包括Bean的类名、作用域、是否懒加载、依赖关系、初始化方法、销毁方法等。
在Bean未实例化之前,咱们的Bean是以BeanDefinition的形式存在的,就是存放一些Bean的定义,然后通过BeanFactory创建。
好,我们从启动类开始看下。
run方法呢,进去会走到这里,别的先不管,我们注意这几处。
前面不是贴了张图,最开始创建了默认的BeanFactory。
然后,我们直接给BeanFactory中的beanDefinitionMap添加监视,关注它的数量变化,看这个map的数量啥时候变多了,变多了,肯定是扫描到需要的BeanDefinition了呗。
或者,你像我这样,把它置顶,反正我们的主要目的是观察在哪个位置数量变化了。
然后,你应该能观察到,在这这两行之间,这个map的size变化了,像我这里就是从6变化到47了。
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
好,那我们现在知道经过prepareContext()
之后,这个map变化了,关键是怎么找到具体的位置?
进去具体的prepareContext()方法,然后逐行执行下,现在我们发现是在this.refresh(context);
这里变化的。
所以你该把断点往后挪,前面的可以取消了。
让你改断点的意思是,逐步精确位置,哪怕中间出问题了,我们也能快速还原现场,你看我现在就可以直接重启,立马就到这里了,而且我知道接下来它会变化了。
这才开始,坚持住哥,别慌。
跟着走几个方法,你会发现进了个大的方法,按刚才的逻辑,缩小断点,这里我先直接打到方法首部和尾部。
为啥我这样搞,首先,这里右侧的监视器已经丢失了。而且,我这个方法执行完,肯定值会变化。
我们先首位打断点,二分找位置。
你看,到这里,我们知道在这之间发生了变化。二分断点法,我直接把尾部的提前到中间。
你看,我们范围已经缩小很多了。
下一步是啥,下一步我肯定是去除掉最开头的那个断点。
你别慌,现在范围缩小了,你把其余断点关闭下,不麻烦,哪怕你重启,那不是立马还原现场?
你看,又定位到一个位置了,在这里变化了。
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
思路是什么,往里面进。
继续继续。
定位到这里变了7,是不是得往方法里面进。
再进。
你看到这个玩意,是不是感觉这是精髓的代码,就在这里变成7了。
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来抽象话术。
咱不懂,但是咱大概能猜到,对吧,这里不就是看if有没有嘛,没有我给你加一个,看起来似乎是框架自身用的。
所以,这里不是我们要的那个关键点,我们的注意力换成,这个size的7咋变46了?
按照之前的思路,先找到7,逐个往后放断点,轻松的调整一下我们的断点,定位出一个区域。
嗯,所以,搞半天,又来到这里。
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();
一样的操作,进invokeBeanDefinitionRegistryPostProcessors,继续缩小范围。
然后呢,中间经过一番小小的摸索,我们就能找到这里。
org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java
的beanDefinitions.add(definitionHolder);
好,这里你注意我左侧config文件夹下的3个aware类,在这里被扫描到加进去BeanDefinition了。
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
那这里的definitionHolder和this.registry都是啥。
- definitionHolder看不懂,反正我们知道它持有BeanDefinition。
- this.registry就是我们的BeanFactory嘛
那其实多点两步也能看到,最后就是把我们的BeanDefinition根据名字来放到BeanFactory中去。
现在我们来回顾下,大概是干嘛了。
- SpringBoot启动的时候呢,会扫描到咱们需要注入的类。
- 把这些类的BeanDefinition信息,放入到BeanFactory。
那我想知道咋就扫出来了,是不是该看这个for。
这个方法就是在判断咱们的一个类是否是候选组件。
候选组件是指那些可以被Spring容器管理的类,比如带有特定注解的类(如 @Component
、@Service
、@Repository
等)。
5.3 BeanDefinitionRegistry
BeanDefinitionRegistry 是Spring中的一个接口,用于注册和管理Bean定义。
它提供了一些方法来动态地向Spring容器注册新的Bean定义或移除现有的Bean定义。
常用的实现类是 DefaultListableBeanFactory
,它既是一个 BeanFactory
,也是一个 BeanDefinitionRegistry
。
我们先微调下代码,我把注册的类改成了MyComponent,就一个类。
好,上一节我们在注册BeanDefinition的时候,不是找到了一个方法吗?
org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java
的registerBeanDefinition
。
这里就是在注册BeanDefinition,但是我们肯定会疑问,这里的registry是啥?看一眼呗。
还记得DefaultListableBeanFactory
吗,它不就是我们在开始提到的,默认BeanFactory。
所以,注册BeanDefinition,就是在往自身的map里面put值。
嗯,最后,来总结下,BeanDefinitionRegistry无非就是这个接口。
实现了这个接口,它必然得实现registerBeanDefinition、removeBeanDefinition、getBeanDefinition这个些方法。
那我们看到一个类实现它,无非就是说明这个类能管理BeanDefinition。
有这个印象就够了。
然后,截止到目前,我们知道了什么?
我们已经知道,咋们SpringBoot程序在启动后。
- 会创建BeanFactory
- 然后会扫描需要注册的Bean
- 最后会加载它们的BeanDefinition到beanFactory自身的beanDefinitionMap中去
之后该干嘛,是不是该创建了?
那我们之前有了解到,像我们的MyComponent这种就是放到BeanFactory的单例池中去。
创建,我们是不是该和之前的思路一样,先在最外层关注下singletonObjects的数量,再看看在哪里发生变化了?
所以是不是很容易定位到一切的尽在refreshContext
这个方法中?然后复刻之前的思路,逐步缩小范围。
是不是很容易的就能找到我们这个myComponent跟org/springframework/beans/factory/support/DefaultListableBeanFactory.java
的preInstantiateSingletons()方法有关?
然后进到实际的doGetBean方法中,我们的关注点是不是应该发生转变吗,看Object beanInstance;这个玩意什么时候有值的?
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 前后进行额外处理。
你看,里面给了你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;
}
}
启动看看,哇靠,你看,是不是跟我们想的一样,前后执行一手。
哎,那我们项目里不是有个MyConfig
吗,它会触发吧?
@Configuration
public class MyConfig {
}
形参这么简单,岂不是很好用,加一行666的输出。
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 ---------------------");
}
}
5.3 Aware
Aware接口主要用于让Bean感知到Spring容器的存在,并获取Spring容器中的一些资源或信息。
很抽象对不,还感知?还获取资源?怎么这么恶心啊这些名词,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也给我们了。
启动一手,可以看到这两个方法确实被执行了。
好了,现在有点印象了对不对,大概就是XxAware,就能给你提供Xx给你让你访问。嗯,只能访问不能改,我给你试了。