Spring Bean作用域揭秘:单例、原型、请求与会话的区别

相信很多朋友在学习Bean的创建和注入时都会有疑问?我们使用不同方式从容器中拿到的Bean是同一个Bean还是不同的Bean,接下来我将为大家解答这个疑问~

概念

用来规定Spring容器创建Bean实例的数量,生命周期与有效范围,也就是Bean什么时候创建,什么时候销毁,和哪个地方可以拿到这个Bean。

在讲解前我们先简单回顾一下Bean的注入和获取:

注入:通过五大类注解或方法注解(@Bean)可以将方法的返回值存放在容器中

五大注解:@Component、@Controller、@Service、@Repository、@Configuration

@Bean的使用方式:setter方法,构造方法...

获取:①ApplicationContext.getBean()②@Autowired③@Resource...


准备工作:

为了更好的展示Bean的作用域,我们需要借助代码来加深理解,需要先创建一个对象,我们就以创建一个Dog的类为例:

java 复制代码
@Data
public class Dog {
    private String name;
}

单例Singleton

Spring在启动前只会创建一次实例,并存放在容器中,无论是getBean()还是通过注解从容器拿到Bean对象,都只会是同一个对象,使用结束后,Spring会自动销毁。一般注入Bean时默认为Single从代码中来理解:

定义一个Configuration类注入Dog类

java 复制代码
@Data
public class Dog {
    private String name;
    public String getRealAddress() {
        // 这里的 this 是当前请求真实绑定的那个 Dog 对象
        return Integer.toHexString(System.identityHashCode(this));
    }
}

在启动类中注入对象

java 复制代码
@SpringBootApplication
public class SpringDemoBeanApplication {


    public static void main(String[] args) {

        ApplicationContext context = SpringApplication.run(SpringDemoBeanApplication.class, args);
        Dog dog1 = context.getBean(Dog.class);
        dog1.setName("旺财");
        System.out.println(dog1.getName());
        Dog dog2 = (Dog)context.getBean("getDog");
        System.out.println(dog2.getName());

    }

}

分别使用类名.class和Bean Name的方式来注入Bean,观察结果:

可以发现,我们明明没有设置dog2的name值,但结果却显示dog1和dog2的name一样,由此可证,默认情况下的Bean是单例模式。

也可通过@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)定义Bean的作用域为单例

原型(多例)Prototype

Spring每次启动都会重新创建一个新的Bean对象,并将新的对象存放在容器中,每次使用getBean()从容器中拿到的对象都会不一样。Spring 不会管理 prototype Bean 的销毁,需要自己手动释放。

复制代码
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
java 复制代码
@Configuration
public class Config {
     @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)//创建一个多例Bean
    public Dog prototypeDog(){
        Dog dog=new Dog();
        return dog;
    }
}
java 复制代码
@RestController
public class Controller {
    @Autowired
    private ApplicationContext context;

    @Resource(name = "prototypeDog")//注入requestDog
    private Dog prototypeDog;

    @RequestMapping("/prototypeDog")
    public String prototypeDog(){
        // 方法 A:通过注入的代理对象调用方法(最推荐,这也是 Spring 期望的用法)
        String realAddr1 = requestDog.getRealAddress();

        // 方法 B:通过 context 获取并调用方法
        Dog contextDog = (Dog) context.getBean("requestDog");
        String realAddr2 = contextDog.getRealAddress();

        return "通过注入代理调用的真实地址:【" + realAddr1 + "】<br/>" +
                "通过context调用的真实地址:【" + realAddr2 + "】";
    }
}

查看结果:

多次请求可以看出,每次请求context得到的Bean都不一样,符合多例模式的定义,为什么由Config注入的Bean每次得到的都是一个对象呢?

原因:如果是从单例中注入一个原型,只会在单例注入时从Spring容器中获取一个Bean对象,后续不会重新获取对象,而Conreller类刚好是一个单例Bean,这就是为什么上面例子中prototypeDog在每次请求中都保持不变的原因。

请求request

在每次请求中,都会创建一个新的Bean对象。在同一个请求中,指向的Bean相同。但是注意,并不是请求的时候就会注入,而是等到需要到这个Bean的时候才会注入。

只有 web 环境生效!非 web 环境退化成单例!

使用@RequestScope可以设置为request作用域

代码:

java 复制代码
@Configuration
public class Config {

    @Bean
    @RequestScope
    public Dog requestDog(){
        Dog dog=new Dog();
        return dog;
    }

}
java 复制代码
@RestController
public class Controller {
    @Autowired
    private ApplicationContext context;

    @Resource(name = "requestDog")//注入requestDog
    private Dog requestDog;

    @RequestMapping("/requestDog")
    public String requestDog(){
        // 方法 A:通过注入的代理对象调用方法(最推荐,这也是 Spring 期望的用法)
        String realAddr1 = requestDog.getRealAddress();

        // 方法 B:通过 context 获取并调用方法
        Dog contextDog = (Dog) context.getBean("requestDog");
        String realAddr2 = contextDog.getRealAddress();

        return "通过注入代理调用的真实地址:【" + realAddr1 + "】<br/>" +
                "通过context调用的真实地址:【" + realAddr2 + "】";
    }
}

观察不同请求的结果:

由此可以看出,不同的请求会创建不同的Bean放入容器中,我们从Spring容器中拿到的对象也不同。

会话session

在每次会话中都会创建一个新的对象,在同一个会话中指向的Bean相同

只有 web 环境生效!非 web 环境退化成单例!

代码:

java 复制代码
@Configuration
public class Config {
   
    @Bean
    @SessionScope
    public Dog sessionDog(){
        Dog dog=new Dog();
        return dog;
    }

}
java 复制代码
@RestController
public class Controller {
 

    @Autowired
    private ApplicationContext context;
    

    @Resource(name = "sessionDog")
    private Dog sessionDog;

    @RequestMapping("/sessionDog")
    public String sessionDog(){
//        Dog dog1=context.getBean(Dog.class);
        Dog contextDog=(Dog) context.getBean("sessionDog");
        String addr1 = Integer.toHexString(System.identityHashCode(sessionDog));
        String addr2 = Integer.toHexString(System.identityHashCode(contextDog));

        return "通过Config得到的Dog:【地址:"+addr1+"】<br/>" + "通过context得到的Dog:【地址:"+addr2+"】";
    }

}

为了观察到不同会话的结果,我们选择使用浏览器和postMan同时观察:

可以看出同一个浏览器的不同请求是属于一个会话,所以三次请求得到的一样,postMan发送请求的cookie和浏览器的cookie不同,所以地址不同。

application全局Web域

整个 Web 应用(Tomcat)只有 1 个实例,和 singleton 很像,但是层级不一样: singleton 是 Spring 容器级别;application 是 ServletContext 级别,多个容器可以共享这个对象。日常开发很少用。

只有 web 环境生效!非 web 环境退化成单例!

总结

作用域 什么时候创建新对象 适用环境
singleton Spring 容器启动(默认),全局仅 1 个 所有环境
prototype 每次 getBean 都新建 所有环境
request 每一次 HTTP 请求新建 仅 Web 环境
session 每一个用户会话新建 仅 Web 环境
application 整个 web 项目只 1 个 仅 Web 环境

面试重点:

1.Controller 是singleton 单例,如果在 Controller @Resource或者@Autowired直接注入 request/session 作用域 Bean,Spring 注入代理对象,代理负责动态获取当前请求 / 会话的真实 Bean。

2.request/session 作用域没有 web 依赖(没有 spring-boot-starter-web)直接失效,退化成单例

3.prototype:Spring不负责销毁;singleton 容器关闭时销毁;request 请求结束自动销毁

4.为什么Bean为prototype时在单例中注入Bean只会在spring容器中获取一次,而Bean为reuqest或者session时却不会这样呢?

因为当Bean的作用域为reuqest或者session时,创建Bean使用代理模式,Spring会创建一个代理对象放在容器中,此时Bean指向的并非一个真实的对象,当创建Bean时,代理对象会从容器中去获得真正的对象,并注入给对象。

相关推荐
学编程就要猛1 小时前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室1 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳2 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠2 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
高级程序源2 小时前
django招聘网站信息爬取与分析系统79704-计算机课程设计、毕业设计
后端·python·mysql·小程序·django·flask·课程设计
伊信2 小时前
Spring AI 基础学习与应用
java·后端
夜郎king2 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz2 小时前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai
careathers2 小时前
【数据结构】队列
java·数据结构