Spring的作用域scope

在2.0之前只有两种singleton和prototype(网上说的,没去验证,那时候还没仔细研究过…),后面增加了session、request、global session三种专门用于web应用程序上下文的Bean

Singleton

这是spring的bean的默认的作用域-单例。但是此单例又与设计模式中的单例模式不一样,设计模式的单例在设计模式的文章中再介绍。

singleton不要简单的理解为全局只有一个,真正的意义是在一个上下文(ApplicationContext)中只有一个。如果在一个项目中有多个ApplicationContext,那么获取的Bean对象就不只一个了

在同一个容器或上下文中,所有对单例bean的请求,只要id与该bean定义相匹配,都会返回同一个实例。简单说就是bean定义为singleton时,ioc容器只会创建该bean的唯一实例,然后存储在单例缓存(singleton cache)中。

singleton的bean是无状态的,只有一份,所以无论哪个线程获取的都是同一个同样的实例,具有线程无关性。Spring使用ThreadLocal解决线程安全问题,这就要求在使用singleton的bean时不能存在属性的改变,如果存在属性改变,就需要慎重使用,采用同步来处理多线程问题,或者考虑使用prototype作用域。

基于上面,我们常用的Controller、Service、Repository、Configuration这些bean基本上都是项目启动就已经初始化完毕,每个bean的属性都已经确定,在运行过程中也不会更改,这样就很适合单例模式。这里再说一下实体类(用@Entity注解修饰的)这个可不是一个bean,试想一下,每次使用实体的时候是不是都是DomainA a = new DomainA();,应该没有人这么用

private DomainA domianA;```
1
2
3
4
5
6
7

使用scope的方法如下:

```java
@Scope("singleton")
@Component
public class MainService {...}

Prototype

相对应singleton,prototype就属于多例模式,每次请求都会创建一个新的实例,相当于new操作。

对于prototype类型的bean,spring只负责创建初始化,不会对整个生命周期负责,随后的所有操作都交给客户端来处理

现在问一个问题,如何声明一个prototype的bean并使用呢?(先不要急着往下看,想一下,就以在ScopeTestController里面注入prototype的PrototypeService来说明)

~
~
~
~
思考中...
~
~
~
~

可能很多人(包括我)开始会以为像以下这种写法就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface PrototypeService {
}

@Service
@Scope("prototype")
public class PrototypeServiceImpl implements PrototypeService {

}

@RestController
public class ScopeTestController {

@Autowired
PrototypeService prototypeService;

@GetMapping("/")
public void testPrototype() {
System.out.println(prototypeService);
}

}

启动项目,两次次请求,查看控制台输出

com.ukirin.idle.web.service.impl.PrototypeServiceImpl@74738f89
com.ukirin.idle.web.service.impl.PrototypeServiceImpl@74738f89

?一样?什么情况?
其实仔细想一下也就明白了错在哪,Controller是一个单例,在启动时就已经把Service注入了,所以不可能改变,当然现在你可以这么改,将Controller同样改为prototype的。那么恭喜你回答正确,但是,这不是我们的目的,我们的目的是要看在单例中如何使用。

方法1:从ApplicationContext里面获取

将Controller里面的代码做以下改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class ScopeTestController {

// @Autowired
// PrototypeService prototypeService;

@Autowired
WebApplicationContext applicationContext;

@GetMapping("/")
public void testPrototype() {
// System.out.println(prototypeService);
System.out.println(applicationContext.getBean(PrototypeService.class));
}

}

这样每次都从上下文中请求一个实例,容器对每个请求都实例化一个新的Service,没有问题

方法2:方法1的变种,采用工厂模式来生成Service

代码就不写了…

方法3:使用代理

很简单,在@Scope里面加上代理模式即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.INTERFACES)
public class PrototypeServiceImpl implements PrototypeService {

}

@RestController
public class ScopeTestController {

@Autowired
PrototypeService prototypeService;

// @Autowired
// WebApplicationContext applicationContext;

@GetMapping("/")
public void testPrototype() {
System.out.println(prototypeService);
// System.out.println(applicationContext.getBean(PrototypeService.class));
}

}

这样,就可以每次获取不同的Service了,这个的原理就是,在初始化Controller的时候并不是讲一个Service的实体注入,而是注入一个代理,当真正调用Service的时候,代理会对其进行依赖解析,并调用真正的实体bean

额外需要注意的一点是,如果注入的不是接口的实现,而是一个类,那么需要将proxyMode = ScopedProxyMode.INTERFACES改为proxyMode = ScopedProxyMode.TARGET_CLASS

request

该作用域将 bean 的定义限制为 HTTP 请求。只在 web-aware Spring ApplicationContext 的上下文中有效

上述的实验结果是每次请求都会输出不一样的结果,在这里可能会与prototype产生困惑,做以下的实验可以解决你的困惑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public interface PrototypeService {
}

@Service
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public class PrototypeServiceImpl implements PrototypeService {

}

public interface MiddleService {
void test();
}

@Service
public class MiddleServiceImpl implements MiddleService {

@Autowired
PrototypeService prototypeService;

@Override
public void test() {
System.out.println("middle : " + prototypeService);
}
}

@RestController
public class ScopeTestController {

@Autowired
MiddleService middleService;

@Autowired
PrototypeService prototypeService;

// @Autowired
// WebApplicationContext applicationContext;

@GetMapping("/")
public void testPrototype() {
System.out.println("controller : " + prototypeService);
// System.out.println(applicationContext.getBean(PrototypeService.class));

middleService.test();
}

}

输出结果为:

controller : com.ukirin.idle.web.service.impl.PrototypeServiceImpl@6b90371c
middle     : com.ukirin.idle.web.service.impl.PrototypeServiceImpl@6b90371c

然后将作用域改为prototype再测试一下
输出结果为:

controller : com.ukirin.idle.web.service.impl.PrototypeServiceImpl@49b2c498
middle     : com.ukirin.idle.web.service.impl.PrototypeServiceImpl@ccb8c47

结果显而易见

session

该作用域将 bean 的定义限制为 HTTP 会话。 只在web-aware Spring ApplicationContext的上下文中有效。

上述的实验结果是一个会话内输出结果是一样的

global-session

该作用域将 bean 的定义限制为全局 HTTP 会话。只在 web-aware Spring ApplicationContext 的上下文中有效。