Spring Bean作用域与并发安全
一、Spring的bean作用域
作用域 | 描述 |
---|---|
单例(singleton ) |
(默认)每一个Spring IoC 容器都拥有唯一的一个实例对象。 |
原型(prototype ) |
一个Bean 定义,每次创建一个新的实例对象。 |
请求(request ) |
一个HTTP 请求会产生一个Bean 对象,也就是说,每一个 HTTP 请求都有自己的Bean 实例。 |
会话(session ) |
限定一个Bean 的作用域为HTTPSession 的生命周期(同一个会话共享一个实例,不同会话使用不同的实例)。 |
全局会话(global-session ) |
限定一个Bean 的作用域为全局HTTPSession 的生命周期(所有会话共享一个实例)。通常用于门户网站场景。 |
其中request
、session
和global-session
三个只在基于web
的Spring ApplicationContext
中可用。
1.1 @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.test.Notepad" scope="prototype"/>
1.2 作用域代理——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.test.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.test.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
是线程安全的。比如SpringMvc
的Controller
、Service
、Dao
等,这些Bean
大多是无状态的,只关注于方法本身。所以结论是Spring
中的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
的根本就是通过大量这种单例构建起系统,以事务脚本的方式提供服务。
2.1 举例说明
@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来封装变量
ThreadLocal<Integer> tl = new 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
。
2.2 小结
1、在@Controller
/@Service
等容器中,默认情况下,scope
值是单例(singleton
)的,也是线程不安全的。
2、尽量不要在@Controller
/@Service
等容器中定义静态变量,不论是单例(singleton
)还是多实例(prototype
)都是线程不安全的。
3、默认注入的Bean
对象,在不设置scope
的时候他也是线程不安全的。
4、一定要定义变量的话,用ThreadLocal
来封装,这个是线程安全的。
三、单例Bean依赖非单例Bean
在使用Spring
时,可能会遇到这种情况:一个单例的Bean
依赖另一个非单例的Bean
。如果简单的使用自动装配来注入依赖,就可能会出现一些问题,如下所示:
单例的ClassA
@Component
public class ClassA {
@Autowired
private ClassB classB;
public void printClass() {
System.out.println("This is Class A:" + this);
classB.printClass();
}
}
非单例的ClassB
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ClassB {
public void printClass() {
System.out.println("This is Class B:" + this);
}
}
这里ClassA
采用了默认的单例scope
,并依赖于ClassB
,而ClassB
的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
可以看到,两个类的hashCode
在三次输出中都是一样。ClassA
的值不变是可以理解的,因为它是单例的,但是ClassB
的scope
是prototype
却也保持hashCode
不变,似乎也成了单例?
产生这种的情况的原因是,ClassA
的scope
是默认的singleton
,因此Context
只会创建ClassA
的bean
一次,所以也就只有一次注入依赖的机会,容器也就无法每次给ClassA
提供一个新的ClassB
。
3.1 方案一:ApplicationContextAware
要解决上述问题,可以对ClassA
做一些修改,让它实现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;
}
}
这样就能够在每次需要到ClassB
的时候手动去Context
里找到新的bean
。再跑一次测试后得到了以下输出:
This is Class A:com.test.ClassA@4df828d7
This is Class B:com.test.ClassB@31206beb
This is Class A:com.test.ClassA@4df828d7
This is Class B:com.test.ClassB@3e77a1ed
This is Class A:com.test.ClassA@4df828d7
This is Class B:com.test.ClassB@3ffcd140
可以看到ClassA
的hashCode
在三次输出中保持不变,而ClassB
的却每次都不同,说明问题得到了解决,每次调用时用到的都是新的实例。
但是这样的写法就和Spring
强耦合在一起了,Spring
提供了另外一种方法来降低侵入性。
3.2 方案二:@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);
3.3 方案三:@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);
}
}