相信很多朋友在学习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时,代理对象会从容器中去获得真正的对象,并注入给对象。