单例模式和原型模式的bean有什么区别?

62

我刚开始学习Spring,读到了这个内容:

一个bean有它的作用域(scope),定义了它在应用程序中的存在方式。

Singleton:表示每个Spring IOC容器中只有一个bean定义对应一个对象实例。

Prototype:表示一个bean定义可以对应多个对象实例。

那么,“对象实例”是什么?


良好的参考资料:https://docs.spring.io/spring-framework/docs/3.0.0.M4/reference/html/ch03s05.html#beans-factory-scopes-prototype - VedantK
9个回答

99

Prototype scope = 每次注入/查找时都会创建一个新对象。它将每次使用new SomeClass()

Singleton scope = (默认) 每次注入/查找时返回相同的对象。此处将实例化一个SomeClass实例,然后每次返回它。

另请参阅:


5
单个上下文每个实例与每个注入一个实例对应,对应用程序有何影响?是否会有并发/线程问题? - Nirmal Mangal
2
关于上述答案的问题: 在单例作用域的情况下,一个对象在启动时创建一次(或可以延迟创建)。那么对于原型作用域呢?是否会在上下文启动时创建任何对象?还是完全基于请求创建? - ankurjhawar

34

让我们通过代码简单查找一下。

以下是一个默认为singleton作用域的TennisCoach Bean。

@Component
@Scope("singleton")
public class TennisCoach implements Coach {

    public TennisCoach(){

    }

    @Autowired
    public void setFortuneService(FortuneService fortuneService) {
        this.fortuneService = fortuneService;
    }

    @Override
    public String getDailyWorkout() {
        return "Practice your backhand volley";
    }

    @Override
    public String getDailyFortune() {
        return "Tennis Coach says : "+fortuneService.getFortune();
    }

}

以下是一个原型作用域的TennisCoach Bean

@Component
@Scope("prototype")
public class TennisCoach implements Coach {

    public TennisCoach(){
        System.out.println(">> TennisCoach: inside default constructor");
    }

    @Autowired
    public void setFortuneService(FortuneService fortuneService) {
        System.out.println(">> Tennis Coach: inside setFortuneService");
        this.fortuneService = fortuneService;
    }

    @Override
    public String getDailyWorkout() {
        return "Practice your backhand volley";
    }

    @Override
    public String getDailyFortune() {
        return "Tennis Coach says : "+fortuneService.getFortune();
    }

}

以下是一个主类:

public class AnnotationDemoApp {

    public static void main(String[] args) {


        // read spring config file
        ClassPathXmlApplicationContext context =
            new ClassPathXmlApplicationContext("applicationContext.xml");

       // get the bean from the spring container
       Coach theCoach = context.getBean("tennisCoach",Coach.class);
       Coach alphaCoach = context.getBean("tennisCoach",Coach.class);
       // call a method on the bean
       System.out.println("Are the two beans same :" + (theCoach==alphaCoach));

       System.out.println("theCoach : " + theCoach);
       System.out.println("alphaCoach: "+ alphaCoach);


       context.close()

    }
}

对于singleton作用域,输出结果如下:

Are the two beans same :true
theCoach : com.springdemo.TennisCoach@2a53142
alphaCoach: com.springdemo.TennisCoach@2a53142

对于原型作用域,输出结果为:

Are the two beans same :false
theCoach : com.springdemo.TennisCoach@1b37288
alphaCoach: com.springdemo.TennisCoach@1a57272

4
好的例子,你什么时候会选择哪个作用域? - VedantK

15

在上面所说的基础上,不要混淆Java中的单例模式。 根据Java规范,单例模式意味着每个JVM只会创建一个该bean的实例。 但是在Spring中,单例模式意味着每个应用程序上下文中只会创建一个该特定bean的实例。 因此,如果您的应用程序有多个上下文,则仍然可以为该bean创建多个实例。


1
我有一个基于你上面回答的问题。假设我在一个MVC web应用项目中工作。如果我在这里为每个会话或请求使用singleton,那么是否会创建一个新对象?而singleton的作用域是在上下文内以及该会话内部? - Naveen Raj
2
部分正确,部分错误。这完全取决于您在配置文件中定义bean时使用的范围。默认情况下,Java将作用域设置为singleton。有五种可用的作用域:
  1. singleton-每个Spring IoC容器返回一个单个bean实例
  2. prototype-每次请求时返回一个新的bean实例
  3. request-每个HTTP请求返回一个单个bean实例。*
  4. session-每个HTTP会话返回一个单个bean实例。*
  5. globalSession-每个全局HTTP会话返回一个单个bean实例。*
- shashi

4

它们都是创建性设计模式。

Singleton(单例)在第一次调用时创建一个新实例,并在后续调用中返回该实例。

Prototype(原型)每次都会返回一个新的实例。


对于单例模式是的,但对于原型模式不是。Spring没有实现GoF的原型设计模式。 - M.T

4
单例作用域:使用单例作用域,根据提供的bean定义只创建一个bean实例,对于后续请求相同的bean,Spring容器将返回同一实例。
从Spring文档中可以看到:

当您定义一个bean定义并将其作为单例进行作用域限定时, Spring IoC容器会创建该bean定义所定义的对象的唯一实例。 此单个实例存储在此类单例bean的缓存中,并且所有后续请求和 引用该命名bean都会返回缓存的对象...

例如: 假设我们已经定义了一个bean accountDao 如下:
<bean id="accountDao" class="" />

还有另外两个 bean 使用了这个 accountDao bean。

<bean id="someBean" ref="accountDao" /> 
<bean id="anotherBean" ref="accountDao" />

Spring首先创建了accountDaobean并将其缓存。然后对于someBeananotherBean,它将提供相同的accountDao实例。

注意:如果在bean定义中没有指定范围,则Singleton是默认范围。

原型范围:对于原型范围,每次请求bean时都会创建并返回一个新的bean实例。这类似于在java中为类调用new运算符。

例如:假设我们已经定义了一个accountDaobean如下:

<bean id="accountDao" class="" scope="prototype"/>

还有另外两个bean,使用了这个accountDao bean

<bean id="someBean" ref="accountDao" /> 
<bean id="anotherBean" ref="accountDao" />

对于someBean和anotherBean,Spring将返回accountDao对象的两个单独实例。

一个重要的区别是,对于原型(prototype)作用域,Spring不管理bean的完整生命周期,清理工作需要由客户端代码完成。

来自Spring文档:

Spring不管理原型bean的完整生命周期:容器实例化、配置和组装原型对象,并将其交给客户端,没有进一步记录该原型实例。因此,虽然初始化生命周期回调方法在所有对象上被调用而不管它们的作用域,但对于原型,配置的销毁生命周期回调不会被调用。客户端代码必须清理原型作用域对象并释放原型bean所持有的昂贵资源。


我该如何为原型bean执行清理操作?据我所知,所有的清理工作都应由JVM负责。 - Sigcont
1
@Sigstop,没错。您会发现https://dev59.com/uWoy5IYBdhLWcg3wo_gn很有用。 - Somnath Musib

3
  1. 单例作用域是默认值。
  2. 单例bean在应用程序上下文初始化期间创建,并始终返回同一个bean。
  3. 原型bean在每次调用时都会被创建。每次调用时,我们都会得到一个新的对象。

注意:如果其他bean引用并使用应用程序上下文调用,则将创建任何作用域的bean。

检查此示例代码。

public class PrototypeClass {

        PrototypeClass()
        {
            System.out.println("prototype class is created"+this.toString());
        }

    }

当构造函数被调用时,这将打印相关文本。
对于下面的代码:
for(int i=0;i<10;i++) {
   PrototypeClass pct= (PrototypeClass) context.getBean("protoClass");
}

原型类创建了Spring.PrototypeClass @ 29774679原型类创建了Spring.PrototypeClass @ 3ffc5af1原型类创建了Spring.PrototypeClass @ 5e5792a0原型类创建了Spring.PrototypeClass @ 26653222原型类创建了Spring.PrototypeClass @ 3532ec19原型类创建了Spring.PrototypeClass @ 68c4039c原型类创建了Spring.PrototypeClass @ ae45eb6原型类创建了Spring.PrototypeClass @ 59f99ea原型类创建了Spring.PrototypeClass @ 27efef64原型类创建了Spring.PrototypeClass @ 6f7fd0e6原型类创建了Spring.PrototypeClass @ 47c62251

Bean定义是

<bean id="protoClass" class="Spring.PrototypeClass" scope="prototype</bean>

我现在把bean定义中的作用域(scope)改为singleton。在上下文初始化期间,构造函数仅被调用一次。接下来,我去掉了作用域属性(scope attribute),发现它的行为和singleton相同。


2
我希望添加一些额外信息,帮助我们找出上述句子中“对象实例”的含义。Spring文档中的这段话试图定义“对象实例”:

当您创建一个bean定义时,您创建了一个用于创建由该bean定义定义的类的实际实例的配方。 bean定义是一个配方的概念很重要,因为它意味着,与类一样,您可以从单个配方创建许多对象实例

因此,根据上面的部分所述,每个bean定义都可以被视为一个类(从面向对象的角度)。根据您在其中定义的数据(例如作用域等),此类(或bean定义)可能只有一个对象实例(仅通过共享实例的单例作用域)或任何数量的对象实例(例如原型作用域,每次请求特定bean时都会创建一个新的bean实例)。

1
原型作用域:每次注入时都会创建一个新对象。
单例作用域:每次注入时都返回相同的对象。
原型作用域适用于所有有状态的bean,而单例作用域应该用于无状态的bean。 让我用我的示例进行解释。请复制并自行运行它以获得清晰的理解。 考虑一个名为Coach的接口。
public interface Coach {

    public String getDailyWorkout();

    public String getDailyFortune();

}

我们有另一个名为TrackCoach的类,它实现了Coach接口。
public class TrackCoach implements Coach {

    private FortuneService fortuneService;


    public TrackCoach(FortuneService fortuneService) {
        this.fortuneService = fortuneService;
    }

    @Override
    public String getDailyWorkout() {
        return "Run a hard 5k";
    }

    @Override
    public String getDailyFortune() {
        return "Just Do it: " + fortuneService.getFortune();
    }    
}

现在有一个FortuneService接口。
public interface FortuneService {

    public String getFortune();

}

它是由我们的 HappyFortuneService 类实现的。
public class HappyFortuneService implements FortuneService {

    @Override
    public String getFortune() {
        return "Today is your lucky day!";
    }

}

让我们使用 Xml 将这两个类进行连接,并将一个类的对象 bean 注入到另一个类中。让我们执行依赖注入。请注意,我们也可以使用 Java 注释来实现这一点。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">


    <!-- Define your beans here -->

    <!--  define the dependency  -->
    <bean id = "myFortuneService"
        class = "com.luv2code.springdemo.HappyFortuneService">
    </bean>

    <bean id = "myCoach"
        class = "com.luv2code.springdemo.TrackCoach"
        scope = "singleton">


        <!-- set up construction injection -->
        <constructor-arg ref = "myFortuneService" />
    </bean>

</beans>

请注意,scope = singleton

现在让我们定义一个BeanScopeDemoApp,它有我们的主方法。

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class BeanScopeDemoApp {

    public static void main(String[] args) {

        // load the spring configuration file 
        ClassPathXmlApplicationContext context = 
                new ClassPathXmlApplicationContext("beanScope-applicationContext.xml");

        // retrieve bean from spring container 
        Coach theCoach = context.getBean("myCoach", Coach.class);

        Coach alphaCoach = context.getBean("myCoach", Coach.class);

        // check if they are the same 
        boolean result = (theCoach == alphaCoach);

        // print out the results 
        System.out.println("\nPointing to the same object: " + result);

        System.out.println("\nMemory location for theCoach: " + theCoach);

        System.out.println("\nMemory location for alphaCoach: " + alphaCoach +"\n");

        // close the context 
        context.close();
    }

}

运行以上代码后,您将看到以下结果:
Pointing to the same object: true

Memory location for theCoach: com.luv2code.springdemo.TrackCoach@16515bb7

Memory location for alphaCoach: com.luv2code.springdemo.TrackCoach@16515bb7

在调用两次后,它指向相同的对象并占用相同的内存位置。现在让我们在Xml文件中更改scope = prototype,保存并再次运行BeanScopeDemoApp。
您将看到以下结果:
Pointing to the same object: false

Memory location for theCoach: com.luv2code.springdemo.TrackCoach@6d4d203d

Memory location for alphaCoach: com.luv2code.springdemo.TrackCoach@627fbcda

调用两次后,它指向不同的对象并占据不同的内存位置。 下面是我刚才所说的一个图形说明。 输入图像描述 输入图像描述

1
讲解得很清楚。但是,您能否告诉我们何时应该使用单例作用域,何时应该使用原型作用域? - Siddhartha
3
所有的答案都告诉我们单例和原型是“什么”,但没有人告诉我们何时使用单例和原型。使用它们的优缺点是什么? - BrownRecluse
1
当您想要维护状态时,请使用“原型”。 - Atul

0

单例模式是在整个应用程序中都使用同一个实例。

原型模式是每次调用 getBean 方法时都会创建一个新的实例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接