本文最后更新于 186 天前 ,文中信息可能已经过时。如有问题请在评论区留言。

Overview

当创建一个 bean definition 时,实际上创建了一个用于创建该 bean definition 所定义类的实例的 recipe (配方)。 将 bean 定义视为 recipe (配方) 的概念很重要,这意味着,你可以从单个配方中创建许多对象实例,就像类一样。

你不仅可以控制从特定 bean definition 创建的对象中插入各种依赖项和配置值,还可以控制从特定 bean definition 创建的对象的作用域。 这种方法强大且灵活,你可以通过配置选择创建对象的作用域,而不是在 Java 类级别固定对象的作用域。

Spring 框架支持六种作用域,其中四种仅在使用 Web-aware ApplicationContext 时可用。 此外,你还可以创建自定义作用域。

下表描述了支持的作用域:

ScopeDescription
singleton(Default) Scopes a single bean definition to a single object instance for each Spring IoC container.
prototypeScopes a single bean definition to any number of object instances.
requestScopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext .
sessionScopes a singe bean definition to the lifecycle of an HTTP Session . Only valid in the context of a web-aware Spring ApplicationContext .
applicationScopes a single bean definition to the lifecycle of a ServletContext . Only valid in the context of a web-aware Spring ApplicationContext .
websocketScopes a single bean definition to the lifecycle of a WebSocket . Only valid in the context of a web-aware Spring ApplicationContext .

The Singleton Scope

单例作用域是 Spring 中的默认作用域

当定义一个 bean 定义,并将该 bean 定义的作用域设置为 Singleton 时,Spring IoC 容器会创建该 bean 定义所定义的对象的一个实例。 这个单一实例被存储在这些单例 bean 的缓存中,所有对于该命名 bean 的后续请求和应用都会返回该缓存的对象。

下图展示了单例作用域的工作原理:

singleton

Spring 框架中的单例 Bean 的概念与设计模式中定义的单例模式有所不同。设计模式中的单例模式将对象的作用域硬编码为每个 ClassLoader 仅创建一个特定类的实例。Spring 中单例的作用域最好描述为每个容器和每个 Bean。这意味着,如果你在一个 Spring 容器中为特定类定义了一个 bean,Spring 容器将只创建一个该 bean definition 所定义类的实例。

要在 XML 中将一个 bean 定义为 Singleton,你可以按照以下示例定义一个 bean:

xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountService" class="com.something.DefaultAccountService"/>

    <!-- the following is equivalent, though redundant (singleton scope is the default) -->
    <bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
</beans>

要在 Spring Boot 中将一个 bean 定义为 Singleton,你可以按照以下示例定义一个 bean:

java
1
2
3
4
5
6
7
8
9
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public class DefaultAccountService {
    // some definition
}

由于单例作用域是 Spring 默认的作用域,因此,对于作用域需要定义为 Singleton 的 bean,无需添加 @Scope 注解。

Singleton Scope 最佳实践

在使用 Singleton Scope 时,你应该注意一些最佳实践:

  1. 无状态✅。尽可能使你的 Singleton Bean 无状态。有状态的 Singleton 会导致与共享状态相关的难以调试的问题,尤其是在多线程环境中。
  2. 线程安全✅。如果你的 Singleton Bean 有状态,请确保它们是线程安全的,因为 Singleton Bean 是跨多个线程共享的。
  3. 延迟初始化✅。如果你的 Singleton Bean 在启动过程中会占用大量资源,请考虑对其使用延迟初始化。这可以通过在 Bean Definition 中添加 @Lazy 注解来实现。

请记住,Singleton Scope 的关键在于了解每个 Spring IoC 容器只有一个实例。因此,与 Singleton Bean 的每次交互都将与相同状态交互,因此应进行相应的设计。

The Prototype Scope

非单例的原型(Prototype)作用域的 bean 部署会在每次请求特定 bean 时创建一个新的 bean 示例。换句话说:当将 bean 注入到另一个 bean 中或通过容器的 getBean() 方法调用请求它时,都会创建一个新的实例。

通常情况下,应该将原型作用域用于所有有状态(stateful)的 bean,而将单例作用域用于无状态(stateless)的 bean。

下图说明了 Spring 的原型(Prototype)作用域:

prototype

DAO (A data access object) 通常不会配置为原型,因为典型的 DAO 不保存任何会话状态。

以下示例在 XML 中将一个 bean 定义为原型:

xml
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
</beans>

以下示例在 Spring Boot 中将一个 bean 定义为原型:

java
1
2
3
4
5
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class DefaultAccountService {
    // some definition
}

与其他作用域不同,Spring 不管理原型 bean 的完整生命周期。容器实例化、配置和组装原型对象,并将其交给客户端,不再记录该原型 bean。因此,尽管对所有对象调用初始化生命周期回调方法(无论作用域如何),但对于原型,配置的销毁生命周期回调方法不会被调用。客户端代码必须清理原型作用域对象并释放原型 bean 持有的昂贵资源。

要让 Spring 容器释放原型作用域 bean 持有的资源,可以尝试使用一个持有需要清理的 bean 引用的自定义 bean 后处理器

在某些方面,Spring 容器对于原型作用域 bean 的角色可以看作是 Java 中的 new 操作符的替代品。在那之后所有生命周期管理都必须由客户端处理。(有关 Spring 容器中 bean 生命周期的详细信息,请参阅生命周期回调

如果在单例作用域的 bean 中注入一个原型作用域 bean,需要额外的处理,请参考 Singleton Beans with Prototype-bean Dependencies

Prototype Scope 最佳实践

  1. 请记住 Spring 不会管理 Prototype Bean 的整个生命周期:不会调用销毁生命周期回调。客户端代码必须清理 Prototype Scope 对象,并释放 Prototype Bean 持有的资源。
  2. 尽量少用 Prototype Scope。每次需要创建一个新的 Bean 实例时,都会耗费大量内存和处理时间,特别是对于重量级的有状态 Bean。
  3. 如果你将具有 Prototype Scope 的 Bean 注入到 Singleton Bean 中,则 Prototype Bean 的行为仍与 Singleton Bean 相同。这是因为 Singleton Bean 只创建一次,因此只会注入 Prototype Bean 的一个实例。要解决整个问题,你可以使用 Spring 的方法注入功能。

Web Aware Scopes

在实践中,很少使用这些作用域。

作用域 requestsessionapplicationwebsocket 仅在 Web Application Context 中可用(例如,XmlWebApplicationContext),如果你尝试在常规的 Spring IoC 容器(例如,ClassPathXmlApplicationContext )中使用这些作用域,将会抛出 IllegalStateException 异常,报告未知的 Bean 作用域。注:常规 Spring Boot 工程可用。

作用域 request 为单个 HTTP 请求创建一个 Bean 实例,而作用域 session 则为 HTTP 会话创建一个 Bean 实例。

作用域 applicationServletContext 的生命周期创建一个 Bean 实例,而作用域 websocket 为特定的 WebScoket 会话创建一个 Bean 实例。

Request Scope

可以使用如下方式定义一个作用域 request 的 Bean:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class HelloMessageGenerator {
    // some definition
}

proxyMode 属性是必要的,因为在实例化 Web Application Context 时,没有活动的请求。Spring 创建一个代理来作为注入依赖,并在需要时在请求中实例化目标 Bean。

此外,还可以使用一个组合的注解 @RequestScope 来充当上述定义的快捷方式(推荐):

java
1
2
3
4
5
6
7
8
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope
public class HelloMessageGenerator {
    // some definition
}

Session Scope

我们也可以用相似的方式定义作用域 session 的 Bean:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class HelloMessageGenerator {
    // some definition
}

同样的,也可以用一个组合注解 @SessionScope 来简化定义:

java
1
2
3
4
5
6
7
8
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

@Component
@SessionScope
public class HelloMessageGenerator {
    // some definition
}

Application Scope

application 作用域为 ServletContext 的生命周期创建一个 Bean 实例。这与 singleton 作用域类似,但是就 Bean 的作用域而言有一个非常重要的区别:

当 Bean 是 application 作用域时,相同的 Bean 实例在运行在同一个 ServletContext 中的多个基于 Servlet 的应用程序之间共享,而 singleton 作用域的 Bean 仅作用于单个应用程序上下文。

你可以按如下方式创建 application 作用域的 Bean:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class HelloMessageGenerator {
    // some definition
}

以简洁的方式(@ApplicationScope) 定义:

java
1
2
3
4
5
6
7
8
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.ApplicationScope;

@Component
@ApplicationScope
public class HelloMessageGenerator {
    // some definition
}

WebSocket Scope

websocket 作用域与 WebSocket 会话的生命周期相关联,并适用于 STOMP over WebSocket 应用程序。详情可参阅 WebSocket Scope

定义 websocket 作用域的 Bean:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {

    @PostConstruct
    public void init() {
        // Invoked after dependencies injected
    }

    // ...

    @PreDestroy
    public void destroy() {
        // Invoked when the WebSocket session ends
    }
}

@Controller
public class MyController {

    private final MyBean myBean;

    @Autowired
    public MyController(MyBean myBean) {
        this.myBean = myBean;
    }

    @MessageMapping("/action")
    public void handle() {
        // this.myBean from the current WebSocket session
    }
}

Custom Scopes

除了以上 Spring 已经定义好的 Scope 之外,你也可以定义自己的 Scope。如果你有兴趣,请参考官方文档。本文不再赘述。

FAQ

Singleton Beans with Prototype-bean Dependencies

当在单例作用域的 bean 依赖原型作用域的 bean 时,请注意依赖项在实例化时解析。因此,如果你将一个原型作用域的 bean 依赖注入到一个单例作用域的 bean 中,会实例化一个新的原型 bean,然后将其依赖注入到单例 bean 中。这个原型实例是唯一供给这个单例作用域 bean 的实例。

然而,假设你希望单例作用域的 bean 在运行时重复获取原型作用域 bean 的新实例。你不能将一个原型作用域的 bean 注入到你的单例 bean 中,因为当 Spring 容器实例化单例 bean 并解析和注入其依赖项时,这种注入只发生一次。如果你需要在运行时多次获得原型 bean 的新实例,请参阅方法注入或按下述 SonarLint 提供的方式注入。

SonarLint

详细内容请参阅:SonarLint Java Rule: RSPEC-6832

错误示例:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Scope("prototype")
public class PrototypeBean {
    public Object execute() {
        //...
    }
}

public class SingletonBean {
    private PrototypeBean prototypeBean;

    @Autowired
    public SingletonBean(PrototypeBean prototypeBean) { // Noncompliant, the same instance of PrototypeBean is used for each bean request.
        this.prototypeBean = prototypeBean;
    }

    public Object process() {
        return prototypeBean.execute();
    }
}

正确示例:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
@Scope("prototype")
public class PrototypeBean {
    public Object execute() {
        //...
    }
}

public class SingletonBean implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Autowired
    public SingletonBean(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public Object process() {
        PrototypeBean prototypeBean = createPrototypeBean();
        return prototypeBean.execute();
    }

    protected PrototypeBean createPrototypeBean() {
        return this.applicationContext.getBean("prototypeBean", PrototypeBean.class);
    }
}

如果在单例作用域的 bean 注入 Request 作用域的 bean 方式如下:

错误示例:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean {
    //...
}

public class SingletonBean {
    @Autowired
    private final RequestBean requestBean; // Noncompliant, the same instance of RequestBean is used for each HTTP request.

    public RequestBean getRequestBean() {
        return requestBean;
    }
}

正确示例:

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean {
    //...
}

public class SingletonBean {
    private final ObjectFactory<RequestBean> requestBeanFactory;

    @Autowired
    public SingletonBean(ObjectFactory<RequestBean> requestBeanFactory) {
        this.requestBeanFactory = requestBeanFactory;
    }

    public RequestBean getRequestBean() {
        return requestBeanFactory.getObject();
    }
}

References