1. 概述
本章主要介绍 Spring 以及 SpringBoot 框架当中一些关于 Bean 的常见面试题,主要分为以下三部分:
- Bean 的作用域
- Bean 的生命周期
- Bean 的自动装配流程
2. Bean 的作用域
我们都知道 Spring 框架内部使用 IoC 容器来管理 Bean,那么如果在不同的类中同时使用@Autowire注解或者使用 ApplicationContext 获取 Bean 的时候,获取到的对象是同一个吗?实际上 Spring 提供了六种作用域可供配置,后面四种只有在 SpringMVC 环境下才会生效
- singleton(单例):这是默认作用域,即实例只会创建一份
- prototype(原型):区别于单例,该模式每次会创建新的实例
- request(请求):在 web 环境下,同一请求内部只有一个实例,多个请求则会创建多个实例
- session(会话):在 web 环境下,同一会话内部的享有一份实例,不同会话则创建新实例
- application(应用):在 web 环境下,同一应用程序内部的享有一份实例,不同应用程序则创建新实例
- websocket:每个 WebSocket 生命周期都会创建新的实例
2.1 准备工作
- 准备 Dog 实体类
public class Dog {
private String name;
public Dog() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
- 准备 Dog 的配置类
@Configuration
public class DogConfig {
@Bean
public Dog singleDog() {
return new Dog();
}
}
2.2 singleton 作用域
在 controller 中编写如下代码:
@RestController
@RequestMapping("/dog")
public class DogController {
@Autowired
private ApplicationContext context;
@Autowired
private Dog singleDog;
@RequestMapping("/single")
public String testSingleDog() {
Dog contextDog = context.getBean("singleDog", Dog.class);
return "singleDog:" + singleDog + ",contextDog:" + contextDog;
}
}
运行代码并在浏览器上进行测试,验证 ApplicationContext 中获取的和依赖注入的对象是否相同,运行效果如下

可以发现,IoC 容器中获取的与依赖注入的对象是一致的,并且无论运行多少次代码,获取到的对象的 hashcode 均是一致的,说明默认的作用域是单例的,接下来我们观察如何显式指定使用singleton作用域,只需要在装配 Bean 的时候添加@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)注解,代码如下:
@Configuration
public class DogConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public Dog singleDog() {
return new Dog();
}
}
2.3 prototype 作用域
接下来测试 prototype 作用域的情况,只需要将对应的注解修改为@Scope=ConfigurableBeanFactory.SCOPE_PROTOTYPE,对应 controller 新增部分代码如下:
@RestController
@RequestMapping("/dog")
public class DogController {
//... 省略其余代码
@Autowired
private Dog prototypeDog;
@RequestMapping("/prototype")
public String testPrototypeDog() {
Dog contextDog = context.getBean("prototypeDog", Dog.class);
return "prototype:" + prototypeDog + ",contextDog:" + contextDog;
}
}


运行多次,效果如上图所示,可以发现每次运行从 ApplicationContext 中获取到的对象都不一样,说明此时采用的是多例(原型)模式
💡 温馨提示:这里@Autowired注入的对象相同是因为,注入阶段在容器启动时,此时该对象是唯一的
2.4 request 作用域
接下来测试 request 作用域的情况,只需要将对应的注解修改为@RequestScope,对应 controller 新增部分代码如下:
@RestController
@RequestMapping("/dog")
public class DogController {
@Autowired
private Dog requestDog;
@RequestMapping("/request")
public String testRequestDog() {
Dog contextDog = context.getBean("requestDog", Dog.class);
return "request:" + requestDog + ",contextDog:" + contextDog;
}
}


运行效果如上图所示,此时每次请求获取的对象是不一样的,但是ApplicatiuonContext 与依赖注入的对象是相同的
2.5 session 作用域
接下来测试 session 作用域的情况,只需要将对应的注解修改为@SessionScope,对应 controller 新增部分代码如下:
@RestController
@RequestMapping("/dog")
public class DogController {
@Autowired
private Dog sessionDog;
@RequestMapping("/session")
public String testSessionDog() {
Dog contextDog = context.getBean("sessionDog", Dog.class);
return "request:" + sessionDog + ",contextDog:" + contextDog;
}
}


运行效果如上图所示,此时同一个会话内部多次请求获取的对象是一样的,且ApplicatiuonContext 与依赖注入的对象也相同的,但是当使用另一个浏览器窗口建立新会话时,获取到的对象是不同的,说明新建会话会创建新的对象
2.6 application 作用域
下来测试 application 作用域的情况,只需要将对应的注解修改为@ApplicationScope,对应 controller 新增部分代码如下:
@RestController
@RequestMapping("/dog")
public class DogController {
@Autowired
private Dog applicationDog;
@RequestMapping("/application")
public String testApplicationDog() {
Dog contextDog = context.getBean("applicationDog", Dog.class);
return "application:" + applicationDog + ",contextDog:" + contextDog;
}
}


运行效果如上图所示,此时多个浏览器窗口访问得到的对象都是一样的,说明在整个应用程序内部实例只有一份
💡 温馨提示:application作用域与singleton的区别就是,singleton在servlet作用域内是单例的,但是在一个web容器内部可以有多个应用程序
3. Bean 的生命周期
3.1 生命周期
Bean 生命周期:指的是一个 Bean 实例从诞生到消亡的全过程,虽然官网没有明确指出 Bean 的生命周期,但是根据源码分析可以看出主要经历会以下五个阶段:
- 实例化(为 Bean 开辟内存空间、调用构造方法)
- 属性赋值:内部如果有属性需要注入则进行装配
- 初始化:
- 执行各种通知方法,比如
BeanNameAware、BeanFactoryAware等接口方法 - 执行初始化方法,比如 XML 定义的 init-method 方法,
@PostConstruct、@BeanPostProcessor
- 执行各种通知方法,比如
- 使用阶段
- 销毁阶段,比如
@PreDestroy注解
3.2 代码演示
接下来就通过编写代码的方式来验证上述生命周期的执行流程,相关代码如下:
@Slf4j
@Component
public class BeanLifeComponent implements BeanNameAware {
private static final Logger logger = LoggerFactory.getLogger(BeanLifeComponent.class);
public Dog dog;
public BeanLifeComponent() {
logger.info("执行构造方法...");
}
@Autowired
@Qualifier("singleDog")
public void setDog(Dog dog) {
logger.info("执行属性注入...");
this.dog = dog;
}
public void use() {
logger.info("bean使用中...");
}
@Override
public void setBeanName(String name) {
logger.info("设置BeanName为:"+ name);
}
@PostConstruct
public void postConstruct() {
logger.info("执行后置方法...");
}
@PreDestroy
public void preDestroy() {
logger.info("即将执行销毁方法...");
}
}
@RestController
@RequestMapping("/life")
public class BeanLifeController {
@Autowired
private BeanLifeComponent beanLifeComponent;
@RequestMapping("/use")
public String use() {
beanLifeComponent.use();
return "OK";
}
}
代码运行效果如下所示:

该效果与我们列举的生命周期全流程相符合
3.3 源码分析
具体代码在<font style="color:rgb(31,35,41);">AbstractAutowireCapableBeanFactory</font>#createBean方法中,在这个方法中调用了核心方法:<font style="color:rgb(31,35,41);">doCreateBean</font>,内部代码经过抽象提炼后如下所示:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
if (instanceWrapper == null) {
// 1.实例化流程(省略其他代码...)
instanceWrapper = this.createBeanInstance(beanName, mbd, args);
}
Object exposedObject = bean;
try {
// 2.属性装配流程(省略其他代码...)
this.populateBean(beanName, mbd, instanceWrapper);
// 3.初始化流程(省略其他代码...)
exposedObject = this.initializeBean(beanName, exposedObject, mbd);
} catch (Throwable var18) {
if (var18 instanceof BeanCreationException bce) {
if (beanName.equals(bce.getBeanName())) {
throw bce;
}
}
throw new BeanCreationException(mbd.getResourceDescription(), beanName, var18.getMessage(), var18);
}
}
其中 createBeanInstance、populateBean、initializeBean 各自对应三个阶段,我们继续追进initializeBean方法,内部代码如下:
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
// 1. 调用通知方法
this.invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = this.applyBeanPostProcessorsBeforeInitialization(bean, beanName);
}
// 2. 调用初始化方法
try {
this.invokeInitMethods(beanName, wrappedBean, mbd);
} catch (Throwable var6) {
throw new BeanCreationException(mbd != null ? mbd.getResourceDescription() : null, beanName, var6.getMessage(), var6);
}
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = this.applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
可以发现在初始化流程中,主要进行了调用各类通知方法以及初始化方法
4. Bean 自动装配流程
4.1 装配代码
现在有一个问题,在 SpringBoot 项目中我们可以直接注入一些特殊的 Bean ,比如 redis 组件的 RedisTemplate,说明该 bean 已经被注册到 IoC 容器当中,如果我们想要注入自己的依赖应该怎么做呢?
4.1.1 方式一
MyBean.java:模拟外部包需要注册到 Ioc 容器的bean
package org.test;
import org.springframework.stereotype.Component;
@Component
public class MyBean {
}
MyBeanController:使用 MyBean 的类(依赖注入)
@RestController
public class MyBeanController {
@Autowired
private MyBean bean;
}
PrincipleApplication:启动类,配置组件扫描路径
@ComponentScan({"org.test", "com.rice.principle"})
@SpringBootApplication
public class PrincipleApplication {
public static void main(String[] args) {
SpringApplication.run(PrincipleApplication.class, args);
}
}
4.1.2 方式二
除了上述使用 ComponentScan 主动配置扫描策略外,还可以使用@Import注解主动导入所需的类,代码如下:
@Import(MyBean.class)
@SpringBootApplication
public class PrincipleApplication {
public static void main(String[] args) {
SpringApplication.run(PrincipleApplication.class, args);
}
}
4.1.3 方式三
方式一与方式二缺点:
- 使用方需要主动使用
@ComponentScan以及@Import配置扫描路径和导入类 - 使用方导入一个外部包就需要配置一次扫描路径和导入一个文件
可以考虑另一种策略,由第三方依赖管理所有需要被扫描的路径,这就需要第三方依赖实现@ImportSelector接口及其对应方法
MyBeanImportSelector:告诉启动类需要扫描哪些路径
@Component
public class MyBeanImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{
"org.test.MyBean"
};
}
}
PrincipleApplication:启动类代码
@Import(MyBeanImportSelector.class)
@SpringBootApplication
public class PrincipleApplication {
public static void main(String[] args) {
SpringApplication.run(PrincipleApplication.class, args);
}
}
4.1.4 方式四
上述代码还可以继续优化,实际上可以由第三方依赖提供注解(将所需要的 bean 装配到 spring 当中),解决策略就是让第三方依赖提供一个注解,启动类只需要使用该注解即可
EnableMyBean:封装注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyBeanImportSelector.class)
public @interface EnableMyBean {
}
EnableMyBean:启动类,使用该注解
@EnableMyBean
@SpringBootApplication
public class PrincipleApplication {
public static void main(String[] args) {
SpringApplication.run(PrincipleApplication.class, args);
}
}
这也是 Spring 使用的方式!
4.2 源码分析
剩下的问题就是 SpringBoot 是如何将所依赖的组件的 bean 装配到 IoC 容器当中的呢?这一切都要从 @SpringBootApplication这个注解说起,让我们一起来分析源码:

可以发现有如下三个子注解:
@ComponentScan:配置扫描包的路径,这里没有显式声明则表示扫描启动类下的包@SpringBootConfiguration:内部使用了@Configuration注解,标识这是一个 Bean@EnableAutoConfiguration:这是自动装配的核心注解
我们继续深入@EnableAutoConfiguration注解分析,发现内部又有下面子注解:

@Import(AutoConfigurationImportSelector.class)这个代码我们已经很熟悉了,现在我们看看内部到底导入了哪些扫描路径,

其中有一行代码是值得关注的:
- List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

从该源码可以看出其中一个扫描配置路径就是META-INF/spring/...内
@AutoConfigurationPackage该注解导入配置文件 AutoConfigurationPackages.Registrar.class

所以该注解主要导入了启动类包下全部的组件
5万+

被折叠的 条评论
为什么被折叠?



