目录
1.1.6 HTTP WebSocket 作用域 (websocket)
1.2.1 直接设置: @Scope("prototype")
1. Bean 的作用域
什么是 Bean 的作用域 ? 我们以前所谈到的作用域就是指程序中变量的可用范围, 例如局部变量的作用域, 就是出了函数就不能用了. 而此处 Bean 的作用域是指在 Spring 中的某种行为模式. 下面通过一个示例来演示 Bean 的作用域.
【代码示例】
Cat 类:
public class Cat {
private int id;
private String name;
private int age;
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;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用方法注解存储 Cat:
@Controller
public class CatBean {
@Bean
public Cat cat() {
Cat cat = new Cat();
cat.setId(1);
cat.setName("加菲猫");
cat.setAge(12);
return cat;
}
}
张三的业务:
@Controller
public class ScopeController {
@Autowired
private Cat cat1;
public void doController() {
System.out.println("do scope controller");
System.out.println("原数据: " + cat1.toString());
cat1.setName("汤姆猫"); // 张三想要修改自己的 cat
System.out.println("新数据: " + cat1.toString());
}
}
李四的业务:
@Controller
public class ScopeController2 {
@Autowired
private Cat cat2;
public void doController() {
System.out.println("do scope controller 2");
System.out.println(cat2.toString()); // 李四想要拿到 Spring 中的加菲猫
}
}
执行启动类:
public class App {
public static void main(String[] args) {
// 1. 得到 Spring 上下文
ApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 执行张三的代码
ScopeController scopeController =
context.getBean("scopeController", ScopeController.class);
scopeController.doController();
System.out.println("=====================================");
// 执行李四的代码
ScopeController2 scopeController2 =
context.getBean("scopeController2", ScopeController2.class);
scopeController2.doController();
}
}
运行结果:
【问题】
从上述示例中分析: 站在张三的角度上, 他只是想要修改自己的 Cat, 将 name 修改为 "汤姆猫", 一打印原数据和新数据都没问题, 而这时李四通过属性注入的方式, 调用 cat 的 toString() 方法, 想要拿到一只 "加菲猫", 却打印出来一只 "汤姆猫", 这就是上述代码示例想要表达的一个问题.
为什么会出现这种情况呢 ?
这里就牵扯到了 Bean 的作用域, Spring 中存储的对象的默认作用域就是单例作用域, 所以就会导致张三觉得自己只修改了自己的 cat1 对象, 并没有动原数据, 却让李四拿到了自己修改后的数据. 实际上 Spring 中只有一份 bean 对象, 而 cat1 和 cat2 的引用都指向这一份 bean , 所以张三对 cat1 的行为就会影响到 Spring 中这一份 bean 的状态.
上述示例就是为了讲明白 Bean 的作用域是指 Bean 在 Spring 整个框架中的某种行为模式, 比如上述的 "单例作用域" (singleton), 就表示 Bean 在整个 Spring 中只有一份, 它是全局共享的, 如果有一个人注入了 bean 对象, 并对其进行了修改, 那么其他人再去注入得到时候, 就都是修改后的数据.
1.1 Bean 的六大作用域
1. singleton:单例作⽤域2. prototype:原型作⽤域(也叫多例作⽤域)3. request:请求作⽤域4. session:会话作⽤域5. application:全局作⽤域6. websocket:HTTP WebSocket 作⽤域
1.1.1 单例作用域 (singleton)
含义: 单例作用域是指在 Spring IoC 容器中只存储一份, 也就是说只有一个实例, 无论我们是通过 @Autowried, @Resource 去获取, 还是通过上下文对象去 getBean(), 拿到的 bean 对象都是同一份. (并且单例作用域是 Spring 中默认的作用域)
场景:: 通常是无状态的 Bean 使用的作用域. (无状态表示 Bean 对象的属性状态不需要修改)
1.1.2 原型作用域 (prototype)
含义: 原型作用域也叫作多例作用域, 每次从 Spring 中获取 Bean 对象, 都会创建一份新的实例, @Autowired, @Resource 注入的对象以及 context 上下文 getBean 拿到的都是不同的 bean 对象.
场景: 通常是有状态的 Bean 使用的作用域 (有状态表示 Bean 对象的属性需要被修改)
1.1.3 请求作用域 (request)
含义: 每一次 HTTP 请求都会创建新的实例, 类似于 prototype.
场景: 一次 HTTP 的请求和响应共享一个 bean. (仅在 Spring MVC 中使用)
1.1.4 会话作用于 (session)
含义: 在一个 HTTP session 中, 定义一个 Bean 实例.
场景: 同一个用户的会话共享 Bean (例如在登录场景中记录一个用户的登录信息) 仅在 Spring MVC 中使用
1.1.5 全局作用于 (application)
含义: 在一个 HTTP Servlet Context 中, 定义一个 Bean 实例
场景: Web 应用的上下文信息, 记录一个应用的共享信息.(仅在 Spring MVC 中使用)
application 作用域和 单例作用域还是有区别的, 它只是同一份上下文对象共享同一个 bean, 当再次创建上下文对象时, 调用 getBean() 就是另一个 Bean 对象了.
1.1.6 HTTP WebSocket 作用域 (websocket)
含义: 在⼀个HTTP WebSocket的⽣命周期中,定义⼀个Bean实例
场景: WebSocket的每次会话中,保存了⼀个 Map 结构的头信息,将⽤来包裹客户端消息头。第⼀次初始化后,直到WebSocket结束都是同⼀个Bean。(仅在 Spring MVC 中使用)
1.2 如何设置 Bean 的作用域
既然我们知道了 Bean 有六大作用域, 那我们应该如何设置 Bean 的作用域呢 ? 使用 @Scope 标签
设置 Bean 的作用域有两种方式:
- 直接设置: @Scope("prototype")
- 使用类似枚举的方式设置: @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
1.2.1 直接设置: @Scope("prototype")
使用前面猫的例子:
【代码示例】
@Controller
public class CatBean {
@Scope("prototype")
@Bean
public Cat cat() {
Cat cat = new Cat();
cat.setId(1);
cat.setName("加菲猫");
cat.setAge(12);
return cat;
}
}
我们只需要在前面的代码中的 CatBean 类中的 @Bean 注解上加上一个 @Scope 注解, 并设置 "prototype" , 此时我们运行程序, 如果李四拿到的是 "加菲猫", 那么就说明此时是多例作用域.
运行结果: 由此可见此时的作用域就是多例作用域.
1.2.2 使用类似枚举的方式设置 Bean 的作用域
依旧使用前面猫的例子:
【代码示例】
@Controller
public class CatBean {
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean
public Cat cat() {
Cat cat = new Cat();
cat.setId(1);
cat.setName("加菲猫");
cat.setAge(12);
return cat;
}
}
此时李四还是能拿到原来的 "加菲猫", 也是成功的将 Bean 的作用域修改成了多例作用域.
2. Spring 的主要执行流程
主要执行流程:
1. 启动 Spring 容器
2. 初始化 Bean 【加载】
3. 将Bean 对象注入到容器中
4. 使用 Bean
最后其实还有销毁 Bean.
3. Bean 的生命周期
1. 实例化 Bean (不等于初始化) 【分配内存空间】
2. 设置属性【依赖注入DI】
3. Bean 的初始化
- 执行各种通知
- 初始化的前置方法. (以前是通过在 xml 中配置 init-method() 方法, 之后改用 @PostConstruct 注解)
- 初始化方法
- 初始化的后置方法
4. 使用 Bean
5.销毁 Bean. (以前通过 xml 的 destroy-method, 之后改用 @PreDestroy 注解)
下面通过代码的方法来观察 Bean 的生命周期:
【代码示例】
public class BeanLifeController implements BeanNameAware {
@Override
public void setBeanName(String s) {
System.out.println("执行各种通知:" + s);
}
/**
* xml 中 init-method 指定的前置方法
*/
public void initMethod() {
System.out.println("执行 init-method 前置方法");
}
/**
* 改用注解后的前置方法
*/
@PostConstruct
public void PostConstruct() {
System.out.println("执行 PostConstruct 前置方法");
}
/**
* 销毁前执行方法
*/
@PreDestroy
public void PreDestroy() {
System.out.println("执行 PreDestroy 销毁方法");
}
public void use() {
System.out.println("使用 bean - 执行 use 方法");
}
}
使用原始的 <bean> 标签设置 bean
<bean id="beanLife" class="com.hl.controller.BeanLifeController"
init-method="initMethod"></bean>
启动类:
public class App {
public static void main(String[] args) {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("spring-config.xml");
// 根据 id 获取 bean 对象
BeanLifeController controller =
context.getBean("beanLife", BeanLifeController.class);
// 使用 bean
controller.use();
// 销毁 bean
context.destroy();
}
}
执行结果:
1. 从代码的运行结果来看, 大致执行顺序还是一致的, init-method() 方法和 postConstruct() 方法的执行先后, 可以理解使用注解的方式是改进后的, 优先级被提高了.
2. 这几个生命周期可以这样理解, 方便我们记住:
- 实例化 Bean --> 【买房】
- 设置属性 --> 【装修】
- Bean 的初始化 --> 【买家电: 桌子, 凳子, 冰箱, 空调......】
- 使用 Bean --> 【入住】
- 销毁 Bean -->【不想住了, 卖房】
【问题】为什么【依赖注入DI】的执行时机要在 【Bean 的初始化之前】?
public class BeanLifeController implements BeanNameAware {
// 依赖注入DI
@Autowired
private UserService userService;
@Override
public void setBeanName(String s) {
System.out.println("执行各种通知:" + s);
}
// 初始化的前置方法
@PostConstruct
public void PostConstruct() {
// 在初始化的前置方法中调用
userService.doUserService();
System.out.println("执行 PostConstruct 前置方法");
}
}
上述代码在初始化的前置方法中使用注入的 Bean, 如果是先初始化 Bean, 就会导致空指针异常, 我初始化方法中需要使用到注入的 Bean , 那么一定是先执行【依赖注入】, 在执行【初始化】。