Bean的作用域和生命周期

发布于:2025-02-11 ⋅ 阅读:(211) ⋅ 点赞:(0)

目录

Bean 的作用域

单例作用域

多例作用域

请求作用域

会话作用域

全局作用域

Bean 的生命周期


Bean 的作用域

在 Spring IoC和DI-CSDN博客 中,我们学习了 Spring 是如何帮助我们管理对象的:

(1)通过 @Controller、@Service、@Repository、@Component、@Configuration、@Bean 来声明 Bean

(2)通过 ApplicaationContext 或 BeanFactory 来获取Bean

(3)通过 属性注入、Setter 方法注入 或 构造方法注入等为程序注入所依赖的 Bean 对象

例如:

public class Student {
    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

使用 @Bean 声明 bean,将 bean 存储在 Spring 容器中:

@Component
public class StudentBeanConfig {
    @Bean
    public Student student() {
        return new Student();
    }
}

从 Spring 容器中获取 Bean:

@SpringBootApplication
public class SpringBeanApplication {

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringBeanApplication.class, args);
        Student student = context.getBean(Student.class);
        System.out.println(student);
    }
}

也可以直接注入 ApplicationContext,获取 Spring 容器,从而获取 Bean:

@SpringBootTest
class SpringBeanApplicationTests {
    @Autowired
    private ApplicationContext context;

    @Test
    void contextLoads() {
        Student student = context.getBean(Student.class);
        System.out.println(student);
    }
}

从 Spring 容器中多次获取 Bean:

@SpringBootTest
class SpringBeanApplicationTests {
    @Autowired
    private ApplicationContext context;

    @Test
    void contextLoads() {
        Student student = context.getBean(Student.class);
        System.out.println(student);
        Student student1 = context.getBean(Student.class);
        System.out.println(student1);
    }
}

运行并观察结果:

可以看到,两次输出的 bean 地址值是相同的,也就是说,每次从 Spring 容器中取出的对象是同一个

这样的模式也称为单例模式(一个类只有一个实例,多次创建也不会创建出多个实例)

默认情况下,Spring 容器中的 bean 都是单例的,这种行为模式,我们就称为 Bean 的作用域

 Bean 的作用域是指 Bean 在Spring 框架中的某种行为模式

就如上述单例作用域,表示的是 Bean 在整个 Spring 中只有一份,是全局共享的。在一个方法中修改了它的值,其他方法读取到的就是被修改后的值:

@SpringBootTest
class SpringBeanApplicationTests {
    @Autowired
    private ApplicationContext context;

    @Test
    void contextLoads() {
        Student student = context.getBean(Student.class);
        student.setId(1);
        System.out.println(student.getId());
        student.setId(2);
        System.out.println(student.getId());

        Student student1 = context.getBean(Student.class);
        System.out.println(student.getId());
    }
}

运行结果:

student 和 student1 是同一个对象,student1 拿到了 student 设置的值

那能不能让 student 和 student1 为不同对象,也就是每次获取的 bean 都是一个新的对象呢?

这就是 Bean 的不同作用域了

在 Spring 中支持 6 种作用域,其中,后 4 种在 Spring MVC 环境下才生效

作用域 说明
singleton(单例作用域) 每个 Spring Ioc 容器内同名称的 bean 只有一个实例(默认)
prototype(原型/多例作用域) 每次使用 bean 时都会创建新的实例
request(请求作用域) 每个 HTTP 请求生命周期内,创建新的实例(web 环境中)
session(会话作用域) 每个 HTTP Session 生命周期内,创建新的实例(web 环境中)
application(全局作用域) 每个 ServletContext 生命周期内,创建新的实例(web 环境中)
websocket(HTTP WebSocket 作用域) 每个 WebSocket 生命周期内,创建新的实例(web 环境中)

接下来,我们分别来看不同作用域下的 Bean (由于  websocket 作用域很少使用,在这里就不进实现观察了)

定义不同作用域的 Bean:

使用 @Scope 定义 Bean 的作用域: 

@Component
public class StudentBeanConfig {
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Student singleStudent() {
        return new Student();
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Student prototypeStudent() {
        return new Student();
    }

    @Bean
    @RequestScope
    public Student requestStudent() {
        return new Student();
    }

    @Bean
    @SessionScope
    public Student sessionStudent() {
        return new Student();
    }
    
    @Bean
    @ApplicationScope
    public Student applicationStudent() {
        return new Student();
    }
}

 在 ConfigurableBeanFactory 中只定义了 单例作用域 和 多例作用域

而在定义其他作用域的 Bean 时,可以使用对应的注解 

例如,请求作用域,可以使用 @RequestScope

我们来看 @RequestScope 注解:

因此,也可以使用 @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 

其中,proxyMode 用来为 spring bean 设置代理proxyMode = ScopedProxyMode.TARGET_CLASS 表示这个 Bean 基于 CGLIB 实现动态代理Request、session 和 application 作用域的 Bean 需要设置 proxyMode

同理,

@SessionScope 等同于 @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)

@ApplicationScope 等同于 @Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)

接着,我们就来测试不同作用域的 bean 获取到的对象是否相同

@RestController
@RequestMapping("/student")
public class StudentController {
    @Resource(name = "singleStudent")
    private Student singleStudent;
    @Resource(name = "prototypeStudent")
    private Student prototypeStudent;
    @Resource(name = "requestStudent")
    private Student requestStudent;
    @Resource(name = "sessionStudent")
    private Student sessionStudent;
    @Resource(name = "applicationStudent")
    private Student applicationStudent;
    @Autowired
    private ApplicationContext context;

    @RequestMapping("/single")
    public String single() {
        Student student1 = (Student) context.getBean("singleStudent");
        return "student: " + singleStudent + "</br> contextStudent: " + student1;
    }

    @RequestMapping("/prototype")
    public String prototype() {
        Student student1 = (Student) context.getBean("prototypeStudent");
        return "student: " + prototypeStudent + "</br> contextStudent: " + student1;
    }

    @RequestMapping("/request")
    public String request() {
        Student student1 = (Student) context.getBean("requestStudent");
        return "student: " + requestStudent + "</br> contextStudent: " + student1;
    }

    @RequestMapping("/session")
    public String session() {
        Student student1 = (Student) context.getBean("sessionStudent");
        return "student: " + sessionStudent + "</br> contextStudent: " + student1;
    }

    @RequestMapping("/application")
    public String application() {
        Student student1 = (Student) context.getBean("applicationStudent");
        return "student: " + applicationStudent + "</br> contextStudent: " + student1;
    }
}

我们让每个请求都获取两次 Bean

无论是 @Autowired 还是 context.getBean 都是从 Spring 容器中获取对象

接下来,我们运行程序,观察 Bean 的作用域

单例作用域

访问 http://127.0.0.1:8080/student/single,观察单例作用域

 @Autowired 和 context.getBean 得到的是同一个对象,且多次访问得到的也是同一个对象

多例作用域

访问 http://127.0.0.1:8080/student/prototype,观察多例作用域:

@Autowired 和 context.getBean 得到的是不同对象,且多次访问,使用 context.getBean 得到的是不同对象,而使用 @Autowired 得到的是同一个对象

这是因为使用 @Autowired 注入的对象在 Spring 容器启动时,就已经注入了,所有多次请求也不会发生变化

而 context.getBean 则是在访问接口时才获取对象,因此多次请求获取到的对象不同

请求作用域

访问 http://127.0.0.1:8080/student/request,观察请求作用域

一次请求中, @Autowired  context.getBean 得到的是相同对象,但每次请求,都会重新创建对象,因此不同请求得到的对象是不同的

会话作用域

访问 http://127.0.0.1:8080/student/session,观察会话作用域

多次请求,得到的是相同对象

我们换一个浏览器访问,就会发现此时就创建了新的对象:

全局作用域

访问 http://127.0.0.1:8080/student/application,观察 Application 作用域

 

多次访问都是同一个对象

Application scope 就是对于整个 web 容器来说,bean 的作用域是 ServletContext 级别的

这与 singleton 类似,但与 singletion 的区别在于:Application scope 是 ServletContext 的单例,而 singleton 是一个 ApplicationContext 的单例,在一个 web 容器中 ApplicationContext 可以有多个

在了解了 Bean 的作用域后,我们继续学习 Bean 的生命周期

Bean 的生命周期

生命周期指的是一个对象从诞生到销毁的整个生命过程,这个过程就叫做一个对象的生命周期

Bean 的生命周期可以分为以下 5 个部分:

1. 实例化(为 Bean 分配内存空间,实例化一个 Bean 对象)

2. 属性赋值(Bean 的注入和装配,设置相关属性和依赖)

3. 初始化(执行各种通知 和 初始化方法)

4. 使用 Bean

5. 销毁 Bean(执行销毁容器的各种方法)

在 Spring 中,可以通过以下几种方式来指定 Bean 的初始化方法

1. 使用 @PostConstruct 注解

2. 实现 InitializingBean 接口的 afterPropertiesSet() 方法

3. 在 xml 配置文件中使用 init-method 属性指定

 销毁容器的方法:

1. 使用 @PreDestory 注解

2. 实现 DisposableBean 接口的 destroy 方法

3. 在 xml 配置文件中使用 destroy-method 属性指定

实例化属性赋值 对应 构造方法setter 方法 的注入。而 初始化 销毁 是用户能够自定义扩展的两个阶段,如,可以在实例化之后,类加载之前进行自定义事件处理

其执行流程如下:

接下来,我们就来通过代码观察一下 Bean 的生命周期

@Component
public class BeanLifeComponent implements BeanNameAware {
    private Student student;
    public BeanLifeComponent() {
        System.out.println("执行构造函数...");
    }

    // setter 方法注入
    @Autowired
    public void setStudent(Student student) {
        this.student = student;
        System.out.println("执行 setter 方法...");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("执行 setBeanName 方法 " + name);
    }

    // 初始化
    @PostConstruct
    public void init() {
        System.out.println("初始化...");
    }

    // 使用
    public void use() {
        System.out.println("执行 use 方法...");
    }

    // 销毁前执行方法
    @PreDestroy
    public void destroy() {
        System.out.println("执行 destroy 方法...");
    }
}

进行测试:

@SpringBootTest
class SpringBeanApplicationTests {
    @Autowired
    private ApplicationContext context;

    @Test
    void beanLife() {
        BeanLifeComponent bean = context.getBean(BeanLifeComponent.class);
        bean.use();
    }
}

运行并观察结果:

可以看到,先执行了 构造方法,为其分配了内存空间,接着执行了 setter 方法,进行了属性赋值,然后再执行了 通知方法(如 setBeanName),接下来,执行 初始化方法(@PostConstruct

执行完初始化方法之后,此时才进行启动(Started SpringBeanApplicationTests in 1.4 seconds (process running for 2.214))

此时,执行了 use 方法(bean.use())

最后,进行销毁(@PreDestroy)

我们通过源码来进一步看 Bean 的实例化属性赋值 以及 初始化过程

先找到 AbstractAutowireCapableBeanFactory 类:

 AbstractAutowireCapableBeanFactory 继承自 AbstractBeanFactory,并实现了 AutowireCapableBeanFactory 接口,主要负责 实例化 bean属性填充调用 Bean 的初始化方法

接着,在 AbstractAutowireCapableBeanFactory 中找到 createBean 方法:

 createBean 的主要功能是创建一个 bean 实例、进行属性注入、进行后置处理等

接着,我们在 createBean 方法中找到 doCreateBean 方法:

查看 doCreateBean 方法:

在  doCreateBean 方法中,会调用 createBeanInstance 方法,创建指定的 bean

接着,会调用 populateBean 方法,完成 bean 的属性赋值

完成 属性赋值之后,就会调用 initializeBean 方法,进行初始化

这三个方法也就与三个声明周期阶段一一对应:

createBeanInstance  -> 实例化

populateBean -> 属性赋值

initializeBean -> 初始化

createBeanInstance

在 createBeanInstance 中,会根据 bean 的配置(如 构造器参数、工厂方法等)来创建 Bean 实例

 populateBean

在 populateBean 中,会对 bean 的属性进行填充,将各个属性注入

initializeBean:

在 initializeBean 方法中,会先执行 invokeAwareMethods 方法,也就是 BeanNameAwareBeanClassLoaderAware BeanFactoryAware 方法:

接着是 applyBeanPostProcessorsBeforeInitialization 方法:

applyBeanPostProcessorsBeforeInitialization 的作用是在 Bean 的初始化阶段之前,调用所有 BeanPostProcessor 实现类的 postProcessBeforeInitialization方法,从而对 Bean 实例进行进一步的修改或增强,如:修改 Bean 的属性、对 Bean 进行代理、记录日志等

也就是说,applyBeanPostProcessorsBeforeInitialization 允许我们在 Bean 完成依赖注入之后正式初始化之前,对 Bean 进行处理和修改,也就为我们提供了在 Bean 初始化之前执行逻辑的机会,例如 AOP 增强、属性设置、日志记录等

然后是 invokeInitMethods 方法:

 在 invokeInitMethods 方法中,主要是执行 bean 的初始化逻辑

bean instanceof InitializingBean:判断当前 Bean 是否实现了 InitializingBean 接口,若实现了该接口,调用 afterPropertiesSet 方法

getInitMethodNames:获取指定的初始化方法名称

invokeCustomInitMethod:调用指定的初始化方法


网站公告

今日签到

点亮在社区的每一天
去签到