Spring丨Bean的类型与作用域

前言

什么是 Bean?Spring 官方文档在开篇 Introduction to the Spring IoC Container and Beans 对 bean 的解释是:

In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container.
A Spring IoC container manages one or more beans. These beans are created with the configuration metadata that you supply to the container (for example, in the form of XML <bean/> definitions).

也就是说,bean 是由 Spring IOC 容器实例化、组装和管理的 Java 对象,bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。
对此,我们经常使用 @Component 注解或 @Bean 注解或在 XML 文件中定义 Bean,这都是普通类型的 Bean。

If you have complex initialization code that is better expressed in Java as opposed to a (potentially) verbose amount of XML, you can create your own FactoryBean, write the complex initialization inside that class, and then plug your custom FactoryBean into the container.

SpringFramework 考虑到一些特殊的设计:Bean 的创建需要指定一些策略,或者依赖特殊的场景来分别创建,也或者一个对象的创建过程太复杂,使用 xml 或者注解声明也比较复杂。这种情况下,如果还是使用普通的创建 Bean 方式,以我们现有的认知就搞不定了。于是,SpringFramework 在一开始就帮我们想了办法,可以借助 FactoryBean 来使用工厂方法创建对象。

FactoryBean

FactoryBean<T> 本身是一个接口,它本身就是一个创建对象的工厂。如果 Bean 实现了 FactoryBean 接口,则它本身将不再是一个普通的 Bean ,不会在实际的业务逻辑中起作用,而是由创建的对象来起作用。
在 IDEA 中点击打开 FactoryBean<T> 接口,可以看到它提供了三个方法。其中,有一个方法 isSingleton,默认是 true ,代表默认是单实例的。

package org.springframework.beans.factory;

import org.springframework.lang.Nullable;

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

FactoryBean的使用

假设一个场景:小孩子要买玩具,由一个玩具生产工厂来给这个小孩子造各种各样的玩具。
创建一个儿童类:

@Component
public class Child {
    // 想玩球
    private String wantToy = "ball";

    public String getWantToy() {
        return wantToy;
    }
}

创建几个玩具,包括一个抽象类和两个实现类(为了偷懒,不写多):

// 抽象类
public abstract class Toy {
    private String name;

    public Toy(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Toy{" +
                "name='" + name + '\'' +
                '}';
    }
}

// 玩具球类
public class Ball extends Toy {

    public Ball(String name) {
        super(name);
    }
}

// 玩具汽车类
public class Car extends Toy {
    
    public Car(String name) {
        super(name);
    }
}

然后,创建一个玩具工厂类 ToyFactoryBean,让它实现 FactoryBean 接口,并覆写其中的方法:

public class ToyFactoryBean implements FactoryBean<Toy> {

    private Child child;

    // setter 注入
    public void setChild(Child child) {
        this.child = child;
    }

    @Override
    public Toy getObject() throws Exception {
        switch (child.getWantToy()) {
            case "ball":
                return new Ball("ball");
            case "car":
                return new Car("car");
            default:
                // SpringFramework 2.0 开始允许返回 null
                return null;
        }
    }

    @Override
    public Class<?> getObjectType() {
        return Toy.class;
    }

    @Override
    public boolean isSingleton() {
        return FactoryBean.super.isSingleton();
    }
}

接着,使用注解的方式注册 Bean:

@Configuration
@ComponentScan("com.study.beanType")
public class BeanTypeConfiguration {

    @Bean
    public Child child() {
        return new Child();
    }

    @Bean
    public ToyFactoryBean toyFactory() {
        ToyFactoryBean toyFactory = new ToyFactoryBean();
        toyFactory.setChild(child());
        return toyFactory;
    }
}

最后,测试运行。

public class BeanTypeTest {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanTypeConfiguration.class);
        Toy toy = ctx.getBean(Toy.class);
        System.out.println(toy);
    }
}

作用域

说起作用域,想必都不陌生。在学习 Java 基础之时,就已经接触过了:类变量、局部变量、方法参数变量......作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问,还可以通过 publicprotectedprivate 这些修饰符来限定访问作用域。

public class ScopeReviewTest {
    // 类级别成员
    private static String classVariable = "静态变量c";

    // 对象级别成员
    private String objectVariable = "非静态变量o";

    public static void main(String[] args) throws Exception {
        // 方法级别的成员变量
        String methodVariable = "m";
        for (int i = 0; i < args.length; i++) {
            // 循环体中的局部变量,只在循环体中有用
            String partVariable = args[i];
            // 此处能访问哪些变量?
            System.out.println(partVariable);
            System.out.println(methodVariable);
            System.out.println(classVariable);
            staticTest();
            // 注意:main()方法是静态方法,不能直接调用非静态成员属性和成员方法
        }
        // 此处能访问哪些变量?
        System.out.println(methodVariable);
        System.out.println(classVariable);
        staticTest();
    }

    public void test() {
        // 此处能访问哪些变量?
        System.out.println("这是一个非静态方法");
        System.out.println(classVariable);
        System.out.println(objectVariable);
        staticTest();
    }

    public static void staticTest() {
        // 此处能访问哪些变量?
        System.out.println("这是一个静态方法");
        System.out.println(classVariable);
    }
}

SpringFramework中内置的作用域

Spring 容器在初始化一个 Bean 的实例时,同时会指定该实例的作用域。Spring 为 Bean 定义了 6 种作用域,具体如下。

Singleton

单例 Bean,使用 singleton 定义的 Bean 在 Spring 容器中只有一个实例,这也是 Bean 默认的作用域。
Singleton

Prototype

原型 Bean,每次通过 Spring 容器获取 prototype 定义的 Bean 时,容器都将创建一个新的 Bean 实例。
Prototype

Request

请求 Bean,每次客户端向 Web 应用服务器发起一次请求,Web 服务器接收到请求后,由 SpringFramework 生成一个 Bean ,直到请求结束。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Request 内有效。一次请求创建一个(仅 web 应用可用)。
使用 XML 配置文件:

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>

使用注解声明:

@RequestScope
@Component
public class LoginAction {
    // ...
}

Session

会话 Bean,每个客户端在与 Web 应用服务器发起会话后,SpringFramework 会为之生成一个 Bean ,直到会话过期。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Session 内有效。一个会话创建一个(仅 web 应用可用)。
使用 XML 配置文件:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

使用注解声明:

@SessionScope
@Component
public class UserPreferences {
    // ...
}

Application

应用 Bean,每个 Web 应用在启动时,SpringFramework 会生成一个 Bean ,直到应用停止(也叫 global-session)。一个 Web 应用创建一个(仅 web 应用可用)。
使用 XML 配置文件:

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

使用注解声明:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}

WebSocket

WebSocket Bean,每个客户端在与 Web 应用服务器建立 WebSocket 长连接时,SpringFramework 会为之生成一个 Bean ,直到断开连接。一个 websocket 会话创建一个(仅 web 应用可用)。

演示

因为 singleton 和 prototype 是最常用的两种,所以这里只对这两种做演示(为了偷懒)。

singleton 作用域

当一个 Bean 的作用域为 singleton 时,Spring 容器中只会存在一个共享的 Bean 实例,并且所有对 Bean 的请求,只要 id 与该 Bean 定义相匹配,就只会返回 Bean 的同一个实例。
使用注解驱动式演示,创建两个普通了类 ChildToy

public class Child {
    private Toy toy;

    public void setToy(Toy toy) {
        this.toy = toy;
    }
    
    // 方便输出验证
    @Override
    public String toString() {
        return "Child{" +
                "toy=" + toy +
                '}';
    }
}

// Toy 中标注@Component注解
@Component
public class Toy {}

接着,创建配置类,同时注册两个 Child ,代表现在有两个儿童:

@Configuration
@ComponentScan("com.study.scope.bean")
public class BeanScopeConfiguration {

    @Bean
    public Child child1(Toy toy) {
        Child child = new Child();
        child.setToy(toy);
        return child;
    }

    @Bean
    public Child child2(Toy toy) {
        Child child = new Child();
        child.setToy(toy);
        return child;
    }
}

最后,编写启动类,驱动 IOC 容器,并获取其中的 Child ,打印里面的 Toy

public class SingletonTest {
    public static void main(String[] args) throws Exception {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanScopeConfiguration.class);
        // 两个不同的 Child 持有同一个 Toy
        ctx.getBeansOfType(Child.class).forEach((name, child) -> {
            System.out.println(name + " : " + child);
        });
    }
}

控制台输出以下内容:

child1 : Child{toy=com.study.scope.bean.Toy@4a83a74a}
child2 : Child{toy=com.study.scope.bean.Toy@4a83a74a}

那是类名和 System.identityHashCode(),用 @ 字符分隔。它通常是指对象的初始内存地址,这说明是同一个 Toy

prototype 作用域

在 XML 配置文件中,要将 Bean 定义为 prototype 作用域,只需将 <bean> 元素的 scope 属性值定义成 prototype,示例如下所示:

<!-- 指定为原型 Bean -->
<bean id="person" class="com.study.scope.Person" scope="prototype"/>

如果使用注解,给上面的 Toy 类上标注一个额外的注解:@Scope ,并声明为原型类型:

@Component
@Scope("prototype")
public class Toy {
    
}

其他的代码都不需要改变,直接运行 main 方法,发现控制台打印的两个 Toy 确实不同:

child1 : Child{toy=com.linkedbear.spring.bean.b_scope.bean.Toy@18a70f16}
child2 : Child{toy=com.linkedbear.spring.bean.b_scope.bean.Toy@62e136d3}

参考

Spring 官方文档
掘金 - 从 0 开始深入学习 Spring
打赏
评论区
头像
文章目录