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也不会被再次创建了(注入时创建,而注入只有一次)。

posted on 2020-02-26 14:34  疯狂的小萝卜头  阅读(708)  评论(0编辑  收藏  举报