Spring 中 Bean 的作用域和生命周期

Source

目录

1. Bean 的作用域

1.1 Bean 的六大作用域

1.1.1 单例作用域 (singleton)

1.1.2 原型作用域 (prototype)

1.1.3 请求作用域 (request)

1.1.4 会话作用于 (session)

1.1.5 全局作用于 (application)

1.1.6 HTTP WebSocket 作用域 (websocket)

1.2 如何设置 Bean 的作用域

1.2.1 直接设置: @Scope("prototype")

1.2.2 使用类似枚举的方式设置 Bean 的作用域

2. Spring 的主要执行流程

3. Bean 的生命周期


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 作⽤域
前两种作用域是在普通的 Spring 项目中使用, 后四种作用域存在于 Spring MVC 项目中.(前四种要知道)

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 , 那么一定是先执行【依赖注入】, 在执行【初始化】。