Bean的作用域

  1. Spring应用上下文中所有Bean都是使用单例(singleton)模式创建的。
  2. 在大多数情况下,单例Bean是很理想的方案。初始化和垃圾回收对象实例所带来的成本都是很低的。在大多数情况下,单例Bean是很理想的方案。初始化和垃圾回收对象实例所带来的成本都是很低的。
  3. 但有时候,你可能会发现,所使用的类是易变的(mutable),他们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的Bean就不是什么好主意了,因为对象会被污染,然后重用被污染的对象时会出现意想不到的问题。
  4. Spring已经给出了解决方案。Spring定义了多种作用域,可以基于这些作用域创建bean,如下:
作用域名称 描述
单例(Singleton) 在整个应用中,只会创建一个bean的实例
原型(Prototype 每次注入或者通过上下文获取的时候,都会创建一个新的bean实例
会话(Session) 在Web应用中,为每个会话创建一个bean实例
请求(Request) 在Web应用中,为每个请求创建一个bean实例

声明spring bean的作用域需要用到 @Scope注解
下表展示对应上面的各种类型

value 描述
ConfigurableBeanFactory.SCOPE_PROTOTYPE 这个是说在每次注入的时候回自动创建一个新的bean实例
ConfigurableBeanFactory.SCOPE_SINGLETON 单例模式,在整个应用中只能创建一个实例
WebApplicationContext.SCOPE_GLOBAL_SESSION 全局session中的,一般不常用
WebApplicationContext.SCOPE_APPLICATION 在一个web应用中只创建一个实例
WebApplicationContext.SCOPE_REQUEST 在一个请求中创建一个实例
WebApplicationContext.SCOPE_SESSION 每次创建一个会话中创建一个实例

除了value属性外,还有个属性

proxyMode 描述
ScopedProxyMode.INTERFACES 创建一个JDK代理模式
ScopedProxyMode.NO (默认)不进行代理
ScopedProxyMode.TARGET_CLASS 基于类的代理模式

在使用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,因此不是单例的,这时候跑个测试就看出这样写的问题:
输出的结果是:

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。

不那么好的解决方案
要解决上述问题,可以对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);
  }
  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] theMethodName(no-arguments);

参考链接:

  1. https://www.cnblogs.com/lonecloud/p/5937513.html
  2. https://www.jianshu.com/p/fcdcbaace675
  3. https://www.jb51.net/article/140402.htm
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章