Spring bean的作用域及作用域代理和对应示例
bean的作用域
spring组件的注解Scope大约有singleton、prototype、request、session、global session
这么几种常用的场景。该注解可以配合@Component和@Bean一起使用。这里需要特别说明一下,根据源代码显示 Scope注解分为ConfigurableBeanFactory和WebApplicationContext两个大类,
ConfigurableBeanFactory包含
(singleton、prototype)
两种
WebApplicationContext有
(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,request,session,application,servletContext,contextParameters,contextAttributes)
这么几种Scope
- ConfigurableBeanFactory.SCOPE_PROTOTYPE,即“prototype”
- ConfigurableBeanFactory.SCOPE_SINGLETON,即“singleton”
- WebApplicationContext.SCOPE_REQUEST,即“request”
- WebApplicationContext.SCOPE_SESSION,即“session”
他们的含义是:
- singleton和prototype分别代表单例和多例(原型);
- request表示请求,即在一次http请求中,被注解的Bean都是同一个Bean,不同的请求是不同的Bean;
- session表示会话,即在同一个会话中,被注解的Bean都是使用的同一个Bean,不同的会话使用不同的Bean。
使用session和request产生了一个新问题,生成controller的时候需要service作为controller的成员,但是service只在收到请求(可能是request也可能是session)时才会被实例化,controller拿不到service实例。为了解决这个问题,@Scope注解添加了一个proxyMode的属性,有两个值ScopedProxyMode.INTERFACES和ScopedProxyMode.TARGET_CLASS,前一个表示表示Service是一个接口,后一个表示Service是一个类。
作用域代理
对于bean的作用域,有一个典型的电子商务应用:需要有一个bean代表用户的购物车。
如果购物车是单例,那么将会导致所有的用户都往一个购物车中添加商品。
如果购物车是原型作用域的,那么在应用中某个地方往购物车中添加商品,然后到应用中的另外一个地方可能就没法使用了,因为在这里被注入了另外一个原型作用域的的购物车。
就购物车bean而言,会话作用域是最合适的,因为他与给定用户的关联性最大。
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode =ScopedProxyMode.INTERFACES)
public class ShoppingCart {
//todo: dosomething
}
这里我们将value设置成了WebApplicationContext.SCOPE_SESSION常量。这会告诉Spring 为Web应用的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例。但是对于给定的会话只会创建一个实例,在当前会话各种操作中,这个bean实际上相当于单例的。
注意的是,@Scope中使用了proxyMode属性,被设置成了ScopedProxyMode.INTERFACES。这个属性是用于解决将会话或请求作用域的bean注入到单例bean中所遇到的问题。上小结已部分描述,下面将详细阐述其过程。
假设我们将ShoppingCart bean注入到单例StoreService bean的setter方法中:
@Component
public class StoreService {
private ShoppingCart shoppingCart;
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
//todo: dosomething
}
因为StoreService 是个单例bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中。但是ShoppingCart bean是会话作用域,此时并不存在。直到用户进入系统创建会话后才会出现ShoppingCart实例。
另外,系统中会有多个ShoppongCart 实例,每个用户一个。我们并不希望注入固定的ShoppingCart实例,而是希望当StoreService 处理购物车时,它所使用的是当前会话的ShoppingCart实例。
Spring并不会将实际的ShoppingCart bean注入到StoreService,Spring会注入一个ShoppingCart bean的代理。这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委任给会话作用域内真正的ShoppongCart bean。
- 在上面的配置中,proxyMode属性,被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
- 但如果ShoppingCart是一个具体的类而不是接口的话,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.xx.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.xx.ShoppingCart" scope="session"/>
<aop:scoped-proxy proxy-target-class="false"/>
request和session作用域示例
TestScopeApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestScopeApplication {
public static void main(String[] args) {
// TODO Auto-generated method stub
SpringApplication.run(TestScopeApplication.class, args);
}
}
TestScopeController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestScopeController {
@Autowired
TestScopeSessionService testScopeSessionService;
@Autowired
TestScopeRequestService testScopeRequestService1;
@Autowired
TestScopeRequestService testScopeRequestService2;
@RequestMapping(value = "scope/session/{username}", method = RequestMethod.GET)
public void testScopeSession(@PathVariable("username") String username) {
String id = testScopeSessionService.getId();
System.out.println("scope-->session-->" + Thread.currentThread().getId() + "-->" + id);
}
@RequestMapping(value = "scope/request/{username}", method = RequestMethod.GET)
public void testScopeRequest(@PathVariable("username") String username) {
String id = testScopeRequestService1.getId();
System.out.println("scope-->request-->" + Thread.currentThread().getId() + "-->" + id);
id = testScopeRequestService2.getId();
System.out.println("scope-->request-->" + Thread.currentThread().getId() + "-->" + id);
}
}
TestScopeRequestService .java
public interface TestScopeRequestService {
public String getId();
}
TestScopeRequestServiceImpl.java
import java.util.UUID;
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.INTERFACES)
public class TestScopeRequestServiceImpl implements TestScopeRequestService {
private UUID uuid;
public TestScopeRequestServiceImpl() {
uuid = UUID.randomUUID();
}
public String getId() {
return uuid.toString();
}
}
TestScopeSessionService .java
public interface TestScopeSessionService {
public String getId();
}
TestScopeSessionServiceImpl .java
import java.util.UUID;
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.INTERFACES)
public class TestScopeSessionServiceImpl implements TestScopeSessionService {
private UUID uuid;
public TestScopeSessionServiceImpl() {
uuid = UUID.randomUUID();
}
public String getId() {
return uuid.toString();
}
}
测试
- 访问http://localhost:8043/scope/request/aa、http://localhost:8043/scope/request/aa
scope–>request–>20–>496c46d6-b9b1-42db-9820-35ea662b5501
scope–>request–>20–>496c46d6-b9b1-42db-9820-35ea662b5501
scope–>request–>24–>1438d13f-760d-4774-aa04-c643003c2dee
scope–>request–>24–>1438d13f-760d-4774-aa04-c643003c2dee
早一次http请求中,被注解的Bean都是同一个Bean,因此id值相同
- 访问http://localhost:8043/scope/prototype/aa、http://localhost:8043/scope/prototype/bb
- 切换其他浏览器访问http://localhost:8043/scope/prototype/cc
scope–>session–>27–>52198050-c53f-46da-bb08-010adaf326d5
scope–>session–>18–>8e661356-452b-44b7-b723-52b7be6b4a78
scope–>session–>25–>8e661356-452b-44b7-b723-52b7be6b4a78
在同一个会话中,被注解的Bean都是使用的同一个Bean,不同的会话使用不同的Bean
prototype作用域示例
import org.springframework.context.annotation.Scope;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Scope("prototype")
public class TestScopePrototypeController {
private int index = 0; // 非静态
@RequestMapping(value = "scope/prototype/{username}", method = RequestMethod.GET)
public void testScopePrototype(@PathVariable("username") String username) {
System.out.println("scope-->prototype-->" + Thread.currentThread().getId() + "-->" + index++);
}
}
- 访问http://localhost:8043/scope/prototype/aa五次
scope–>prototype–>27–>0
scope–>prototype–>18–>0
scope–>prototype–>20–>0
scope–>prototype–>21–>0
scope–>prototype–>24–>0
每次注入或者通过上下文获取的时候,都会创建一个新的bean实例
TestBean.java
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class TestBean {
}
TestConfiguration.java
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.gm.test")
public class TestConfiguration {
public TestConfiguration() {
System.out.println("--- TestConfiguration --- 初始化完成");
}
}
TestMain.java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TestMain {
public static void main(String[] args) {
// TODO Auto-generated method stub
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(TestConfiguration.class);
context.refresh();
// 获取bean
TestBean tb = (TestBean) context.getBean("testBean");
System.out.println(tb.toString());
// 获取bean
TestBean tb2 = (TestBean) context.getBean("testBean");
System.out.println(tb2.toString());
}
}
- 原型的特殊情况
假如 Service是多例的,但是Controller是单例的。如果给一个组件加上@Scope(“prototype”)注解,每次请求它的实例,spring的确会给返回一个新的。问题是这个多例对象Service是被单例对象Controller依赖的。而单例服务Controller初始化的时候,多例对象Service就已经注入了;当你去使用Controller的时候,Service也不会被再次创建了(注入时创建,而注入只有一次)。