Spring Bean作用域

一、Spring的bean作用域

作用域 描述
单例(singleton) (默认)每一个Spring IoC容器都拥有唯一的一个实例对象
原型(prototype) 一个Bean定义,每次创建一个新对象
请求(request) 一个HTTP请求会产生一个Bean对象,也就是说,每一个HTTP请求都有自己的Bean实例。只在基于web的Spring ApplicationContext中可用
会话(session) 限定一个Bean的作用域为HTTPsession的生命周期(同一个会话共享一个实例,不同会话使用不用的实例)。同样,只有基于web的Spring ApplicationContext才能使用
全局会话(global-session) 限定一个Bean的作用域为全局HTTPSession的生命周期(所有会话共享一个实例)。通常用于门户网站场景,同样,只有基于web的Spring ApplicationContext可用

@Scope注解

单例是默认的作用域,但有些时候并不符合我们的实际运用场景,因此我们可以使用@Scope注解来选择其他的作用域。该注解可以配合@Component和@Bean一起使用

例如,如果你使用组件扫描来发现和声明bean,那么你可以在bean的类上使用@Scope配合@Component,将其声明为原型bean:

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)  
public class Notepad {
    //todo: dosomething  
}

这里使用的是ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。当然你也可以使用@Scope(value = "prototype"),相对而言笔者更喜欢使用SCOPE_PROTOTYPE常量,因为这样使用不易出现拼写错误以及便于代码的维护。

如果你想在Java配置中将Notepad声明为原型bean,那么可以组合使用@Scope和@Bean来指定所需的作用域:

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad() {
    return new Notepad();    
}

如果你使用xml来配置bean的话,可以使用<bean>元素的scope属性来设置作用域:

<bean id="notepad" class="com.lixiang.Notepad" scope="prototype"/>

作用域代理——proxyMode属性

proxyMode是用来配置当前类的代理模式的。主要用于scope非singleton的情况。因为非singleton的bean spring并不会立刻创建对象,如果需要注入时就产生一个代理对象,这时代理模式就起作用了。

public enum ScopedProxyMode {
    DEFAULT,
    NO,
    INTERFACES,
    TARGET_CLASS;

    private ScopedProxyMode() {
    }
}
作用域代理 描述
DEFAULT 不使用代理,如果需要就立刻创建
NO DEFAULT和NO的作用是一样
INTERFACES 使用JDK的动态代理来创建代理对象
TARGET_CLASS 使用CGLIB来创建代理对象

对于bean的作用域,有一个典型的电子商务应用:需要有一个bean代表用户的购物车。

如果购物车是单例,那么将会导致所有的用户都往一个购物车中添加商品。
如果购物车是原型 作用域的,那么在应用中某个地方往购物车中添加商品,然后到应用中的另外一个地方可能就没法使用了,因为在这里被注入了另外一个原型作用域的的购物车。
就购物车bean而言,会话作用域是最合适的,因为他与给定用户的关联性最大。

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, 
    	proxyMode =ScopedProxyMode.INTERFACES)
public class ShippingCart {
    //todo: dosomething
}

这里我们将value设置成了WebApplicationContext.SCOPE_SESSION常量。这会告诉Spring 为Web应用的每个会话创建一个ShippingCart。这会创建多个ShippingCart bean的实例。但是对于给定的会话只会创建一个实例,在当前会话各种操作中,这个bean实际上相当于单例的

要注意的是,@Scope中使用了proxyMode属性,被设置成了ScopedProxyMode.INTERFACES。这个属性是用于解决将会话请求 作用域的bean注入到单例 bean中所遇到的问题。

假设我们将ShippingCart bean注入到单例 StoreService bean的setter方法中:

@Component    
public class StoreService {            
    private ShippingCart shippingCart;
    public void setShoppingCart(ShippingCart shoppingCart) {
        this.shippingCart = shoppingCart;        
    }    
    //todo: dosomething    
}

因为StoreService 是个单例 bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShippingCart bean注入到setShoppingCart()方法中。但是ShippingCart bean是会话作用域,此时并不存在。直到用户进入系统创建会话后才会出现ShippingCart实例。

另外,系统中会有多个ShippingCart 实例,每个用户一个。我们并不希望注入固定的ShippingCart实例,而是希望当StoreService 处理购物车时,它所使用的是当前会话的ShippingCart实例。

Spring并不会将实际的ShippingCart bean注入到StoreService,Spring会注入一个ShippingCart bean的代理。这个代理会暴露与ShippingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShippingCart的方法时,代理会对其进行懒解析并将调用委任给会话作用域内真正的ShippingCart bean

在上面的配置中,proxyMode属性,被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShippingCart接口,并将调用委托给实现bean。
但如果ShippingCart是一个具体的类而不是接口的话,Spring就没法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话我们必须要将proxyMode属性,设置成ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
请求作用域的bean应该也以作用域代理的方式进行注入。

如果你需要使用xml 来声明会话或请求作用域的bean,那么就需要使用<aop:scoped-proxy />元素来指定代理模式。

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
        
  <bean id="cart" class="com.lixiang.bean.ShoppingCart" scope="session"/>
  <aop:scoped-proxy />
  
</beans>

<aop:scoped-proxy />是与@Scope注解的proxyMode属性相同的xml元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib 创建目标类的代理,如果要生成基于接口的代理可以将proxy-target-class属性设置成false,如下:

<bean id="cart" class="com.lixiang.bean.ShoppingCart" scope="session"/>
<aop:scoped-proxy proxy-target-class="false"/>

Spring中的bean是线程安全的吗?

结论:不是线程安全的

Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。

线程安全这个问题,要从单例与原型Bean分别进行说明。

  • 原型Bean:对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。
  • 单例Bean:对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询 以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

spring单例,为什么controller、service和dao确能保证线程安全?

Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理。实际上大部分时间Bean是无状态的(比如Dao)所以说在某种程度上来说Bean其实是安全的。但是如果Bean是有状态的那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 "singleton"改为"protopyte"这样每次请求Bean就相当于是new Bean()这样就可以保证线程的安全了。

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据
      
    controller、service和dao层本身并不是线程安全的,只是如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。

想理解原理可以看看《深入理解JVM虚拟机》,2.2.2节:

Java虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

《Java并发编程实战》第3.2.2节:

局部变量的固有属性之一就是封闭在执行线程中。
它们位于执行线程的栈中,其他线程无法访问这个栈。

所以其实任何无状态单例都是线程安全的。Spring的根本就是通过大量这种单例构建起系统,以事务脚本的方式提供服务

首先问@Controller @Service是不是线程安全的?
答:默认配置下不是的。因为默认情况下@Controller没有加上@Scope,没有加@Scope就是默认值singleton,单例的。意思就是系统只会初始化一次Controller容器,所以每次请求的都是同一个Controller容器,当然是非线程安全的。举个栗子:

@RestController
public class TestController {

    private int var = 0;
    
    @GetMapping(value = "/test_var")
    public String test() {
        System.out.println("普通变量var:" + (++var));
        return "普通变量var:" + var ;
    }
}

在postman里面发三次请求,结果如下:

普通变量var:1
普通变量var:2
普通变量var:3

说明他不是线程安全的。怎么办呢?可以给他加上上面说的@Scope注解,如下:  

@RestController
@Scope(value = "prototype") // 加上@Scope注解,他有2个取值:单例-singleton 多实例-prototype
public class TestController {

    private int var = 0;
    
    @GetMapping(value = "/test_var")
    public String test() {
        System.out.println("普通变量var:" + (++var));
        return "普通变量var:" + var ;
    }
}

这样一来,每个请求都单独创建一个Controller容器,所以各个请求之间是线程安全的,三次请求结果:

普通变量var:1
普通变量var:1
普通变量var:1

加了@Scope注解多的实例prototype是不是一定就是线程安全的呢?  

@RestController
@Scope(value = "prototype") // 加上@Scope注解,他有2个取值:单例-singleton 多实例-prototype
public class TestController {
    private int var = 0;
    private static int staticVar = 0;

    @GetMapping(value = "/test_var")
    public String test() {
        System.out.println("普通变量var:" + (++var)+ "---静态变量staticVar:" + (++staticVar));
        return "普通变量var:" + var + "静态变量staticVar:" + staticVar;
    }
}

看三次请求结果:

普通变量var:1---静态变量staticVar:1
普通变量var:1---静态变量staticVar:2
普通变量var:1---静态变量staticVar:3

虽然每次都是单独创建一个Controller但是扛不住他变量本身是static的呀,所以说呢,即便是加上@Scope注解也不一定能保证Controller 100%的线程安全。所以是否线程安全在于怎样去定义变量以及Controller的配置。所以来个全乎一点的实验,代码如下:  

@RestController
@Scope(value = "singleton") // prototype singleton
public class TestController {

    private int var = 0; // 定义一个普通变量

    private static int staticVar = 0; // 定义一个静态变量

    @Value("${test-int}")
    private int testInt; // 从配置文件中读取变量

    ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal来封装变量

    @Autowired
    private User user; // 注入一个对象来封装变量

    @GetMapping(value = "/test_var")
    public String test() {
        tl.set(1);
        System.out.println("先取一下user对象中的值:"+user.getAge()+"===再取一下hashCode:"+user.hashCode());
        user.setAge(1);
        System.out.println("普通变量var:" + (++var) + "===静态变量staticVar:" + (++staticVar) + "===配置变量testInt:" + (++testInt)
                + "===ThreadLocal变量tl:" + tl.get()+"===注入变量user:" + user.getAge());
        return "普通变量var:" + var + ",静态变量staticVar:" + staticVar + ",配置读取变量testInt:" + testInt + ",ThreadLocal变量tl:"
                + tl.get() + "注入变量user:" + user.getAge();
    }
}

补充Controller以外的代码:
config里面自己定义的Bean:User

@Configuration
public class MyConfig {
    @Bean
    public User user(){
        return new User();
    }
}

我暂时能想到的定义变量的方法就这么多了,三次http请求结果如下:

先取一下user对象中的值:0===再取一下hashCode:241165852
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

先取一下user对象中的值:1===再取一下hashCode:241165852
普通变量var:2===静态变量staticVar:2===配置变量testInt:2===ThreadLocal变量tl:1===注入变量user:1

先取一下user对象中的值:1===再取一下hashCode:241165852
普通变量var:3===静态变量staticVar:3===配置变量testInt:3===ThreadLocal变量tl:1===注入变量user:1

可以看到,在单例模式下Controller中只有用ThreadLocal封装的变量是线程安全的。为什么这样说呢?我们可以看到3次请求结果里面只有ThreadLocal变量值每次都是从0+1=1的,其他的几个都是累加的,而user对象呢,默认值是0,第二交取值的时候就已经是1了,关键他的hashCode是一样的,说明每次请求调用的都是同一个user对象。
下面将TestController 上的@Scope注解的属性改一下改成多实例的:@Scope(value = "prototype"),其他都不变,再次请求,结果如下:

先取一下user对象中的值:0===再取一下hashCode:853315860
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

先取一下user对象中的值:1===再取一下hashCode:853315860
普通变量var:1===静态变量staticVar:2===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

先取一下user对象中的值:1===再取一下hashCode:853315860
普通变量var:1===静态变量staticVar:3===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

分析这个结果发现,多实例模式下普通变量,取配置的变量还有ThreadLocal变量都是线程安全的,而静态变量和user(看他的hashCode都是一样的)对象中的变量都是非线程安全的。也就是说尽管TestController 是每次请求的时候都初始化了一个对象,但是静态变量始终是只有一份的,而且这个注入的user对象也是只有一份的。静态变量只有一份这是当然的咯,那么有没有办法让user对象可以每次都new一个新的呢?当然可以:

public class MyConfig {
    @Bean
    @Scope(value = "prototype")
    public User user(){
        return new User();
    }    
}

在config里面给这个注入的Bean加上一个相同的注解@Scope(value = "prototype")就可以了,再来请求一下看看:

先取一下user对象中的值:0===再取一下hashCode:1612967699
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

先取一下user对象中的值:0===再取一下hashCode:985418837
普通变量var:1===静态变量staticVar:2===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

先取一下user对象中的值:0===再取一下hashCode:1958952789
普通变量var:1===静态变量staticVar:3===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

可以看到每次请求的user对象的hashCode都不是一样的,每次赋值前取user中的变量值也都是默认值0。
下面总结一下:

1、在@Controller/@Service等容器中,默认情况下,scope值是单例-singleton的,也是线程不安全的。
2、尽量不要在@Controller/@Service等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype)他都是线程不安全的。
3、默认注入的Bean对象,在不设置scope的时候他也是线程不安全的。
4、一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的

单例Bean依赖非单例Bean

在使用Spring时,可能会遇到这种情况:一个单例的Bean依赖另一个非单例的Bean。如果简单的使用自动装配来注入依赖,就可能会出现一些问题,如下所示:

单例的Class A

@Component
public class ClassA {
    @Autowired
    private ClassB classB;
 
    public void printClass() {
        System.out.println("This is Class A: " + this);
        classB.printClass();
    }
}

非单例的Class B

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ClassB {
    public void printClass() {
        System.out.println("This is Class B: " + this);
    }
}

这里Class A采用了默认的单例scope,并依赖于Class B, 而Class B的scope是prototype,因此不是单例的,这时候跑个测试就看出这样写的问题:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {ClassA.class, ClassB.class})
public class MyTest {
    @Autowired
    private ClassA classA;

    @Test
    public void simpleTest() {
        for (int i = 0; i < 3; i++) {
            classA.printClass();
        }
    }
}

输出的结果是:

This is Class A: ClassA@282003e1
This is Class B: ClassB@7fad8c79
This is Class A: ClassA@282003e1
This is Class B: ClassB@7fad8c79
This is Class A: ClassA@282003e1
This is Class B: ClassB@7fad8c79

可以看到,两个类的Hash Code在三次输出中都是一样。Class A的值不变是可以理解的,因为它是单例的,但是Class B的scope是prototype却也保持Hash Code不变,似乎也成了单例?

产生这种的情况的原因是,Class A的scope是默认的singleton,因此Context只会创建Class A的bean一次,所以也就只有一次注入依赖的机会,容器也就无法每次给Class A提供一个新的Class B。

方案一:ApplicationContextAware

要解决上述问题,可以对Class A做一些修改,让它实现ApplicationContextAware。

@Component
public class ClassA implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    public void printClass() {
        System.out.println("This is Class A: " + this);
        getClassB().printClass();
    }

    public ClassB getClassB() {
        return applicationContext.getBean(ClassB.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

这样就能够在每次需要到Class B的时候手动去Context里找到新的bean。再跑一次测试后得到了以下输出:

This is Class A: com.devhao.ClassA@4df828d7
This is Class B: com.devhao.ClassB@31206beb
This is Class A: com.devhao.ClassA@4df828d7
This is Class B: com.devhao.ClassB@3e77a1ed
This is Class A: com.devhao.ClassA@4df828d7
This is Class B: com.devhao.ClassB@3ffcd140

可以看到Class A的Hash Code在三次输出中保持不变,而Class B的却每次都不同,说明问题得到了解决,每次调用时用到的都是新的实例。

但是这样的写法就和Spring强耦合在一起了,Spring提供了另外一种方法来降低侵入性。

解决方案二 @Lookup

Spring提供了一个名为@Lookup的注解,这是一个作用在方法上的注解,被其标注的方法会被重写,然后根据其返回值的类型,容器调用BeanFactory的getBean()方法来返回一个bean。

@Component
public class ClassA {
    public void printClass() {
        System.out.println("This is Class A: " + this);
        getClassB().printClass();
    }

    @Lookup
    public ClassB getClassB() {
        return null;
    }
}

可以发现简洁了很多,而且不再和Spring强耦合,再次运行测试依然可以得到正确的输出。
被标注的方法的返回值不再重要,因为容器会动态生成一个子类然后将这个被注解的方法重写/实现,最终调用的是子类的方法。

使用的@Lookup的方法需要符合如下的签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

方案三 @scope

使用@Scope

@Component
public class ClassA {

    @Resource
    private ClassB classB;

    public void printClass() {
        System.out.println("This is Class A: " + this);
        classB.printClass();
    }
}
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ClassB {
    public void printClass() {
        System.out.println("This is Class B: " + this);
    }
}

参考文章

相关文章

这篇文章主要介绍了spring的事务传播属性REQUIRED_NESTED的原...
今天小编给大家分享的是一文解析spring中事务的传播机制,相...
这篇文章主要介绍了SpringCloudAlibaba和SpringCloud有什么区...
本篇文章和大家了解一下SpringCloud整合XXL-Job的几个步骤。...
本篇文章和大家了解一下Spring延迟初始化会遇到什么问题。有...
这篇文章主要介绍了怎么使用Spring提供的不同缓存注解实现缓...