0%

浅谈Spring的多例Bean——Prototype原型

这里介绍在Spring中如何使用多例Bean,及解决注入多例Bean属性后无法更改的问题

abstract.png

楔子

通常在使用Spring时Bean都是单例的,但有时候我们也会有需要使用多例的场景。为此Spring提供了Prototype原型Bean。其会在每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例

众所周知,在Spring中使用@Component、@Service等注解创建的Bean默认都是单例的。如果想使用多例也很简单,只需使用@Scope注解,并将value属性设置为prototype即可

下面我们分别提供一个单例Bean、原型Bean示例

1
2
3
4
5
6
7
8
9
10
package com.aaronzhu.SpringBoot1.PrototypeTest.pojo;

import org.springframework.stereotype.Component;

@Component
public class BlackDog {
private String name;

private Integer age;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.aaronzhu.SpringBoot1.PrototypeTest.pojo;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
@Scope( ConfigurableBeanFactory.SCOPE_PROTOTYPE )
public class BlackMan {
private String name;

private Date birthday;
}

现在分别提供两个Service来验证下效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.aaronzhu.SpringBoot1.PrototypeTest.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackDog;
import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackMan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AService {
@Autowired
private BlackDog blackDog;

@Autowired
private BlackMan blackMan;

public void test() {
for (int i=0; i<3; i++) {
System.out.printf("[Dog Say]: %d -->> %s%n", i, blackDog);
System.out.printf("[Man Say]: %d -->> %s%n", i, blackMan);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.aaronzhu.SpringBoot1.PrototypeTest.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackDog;
import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackMan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BService {
@Autowired
private BlackDog blackDog;

@Autowired
private BlackMan blackMan;

public void test() {
for (int i=0; i<3; i++) {
System.out.printf("[Dog Say]: %d -->> %s%n", i, blackDog);
System.out.printf("[Man Say]: %d -->> %s%n", i, blackMan);
}
}
}

测试用例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.aaronzhu.SpringBoot1.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.service.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PrototypeTest {
@Autowired
private AService aService;

@Autowired
private BService bService;

@Test
public void test1() {
System.out.printf("--------------------------------------- %s -------------------%n", "Test A Service");
aService.test();

System.out.printf("--------------------------------------- %s -------------------%n", "Test B Service");
bService.test();
}
}

测试结果如下所示

  • 对于单例Bean而言,不仅在同一个Service实例中使用的都是同一个实例对象;而且在不同的Service实例中注入的也是同一个实例对象。符合预期
  • 对于多例Bean而言,其只能保证在不同的Service实例中注入不同的实例对象;但在同一个Service实例中,每次使用的是却是同一个实例对象

figure 1.png

解决方案

问题来了,不是说Prototype原型Bean每次都会使用一个新的实例对象么?为什么在同一个Service实例中,每次使用的是却是同一个实例对象呢?原因在于Spring的多例是指在每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例。显然对于某一个Service实例而言,在注入多例Bean的属性时,Spring会创建一个全新的多例Bean进行注入。但一旦完成属性注入后,该Service实例其所使用的多例Bean即会被固定住,后续将不会再发生更改。那有没有办法实现,即使在同一个Service实例当中,每次使用的多例Bean都是不同的对象呢?答案自然是有的

@Scope注解的proxyMode属性

最简单的办法就是通过代理对象,设置@Scope注解的proxyMode属性为 ScopedProxyMode.TARGET_CLASS 即可。下述是一个设置proxyMode属性的多例Bean示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.aaronzhu.SpringBoot1.PrototypeTest.pojo;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Scope(value= ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class BlackCat {
private String name;

private Date birthday;
}

然后在Service中注入该多例Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.aaronzhu.SpringBoot1.PrototypeTest.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackCat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CService {
@Autowired
private BlackCat blackCat;

public void test() {
for (int i=0; i<5; i++) {
System.out.printf("[Cat Say]: %d -->> %s%n", i, blackCat);
}
}
}

测试用例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.aaronzhu.SpringBoot1.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.service.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PrototypeTest {

@Autowired
private CService cService;

@Test
public void test2() {
cService.test();
}

}

测试结果如下所示,符合预期

figure 2.png

@Lookup注解

我们可以将@Lookup注解在一个非静态的方法上,这样每次调用这个方法时Spring会动态地查找、注入相应的Bean实例。当我们不设置@Lookup注解的value属性时,其会根据方法签名中的返回类型去返回相应类型的Bean实例。与此同时,由于该方法的方法体并不会被真正执行,故我们直接return null,也不会影响结果获取。我们先定义一个多例Bean示例

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.aaronzhu.SpringBoot1.PrototypeTest.pojo;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(value= ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class BlackPig {
private String name;

private String sex;
}

然后,在Service中定义一个添加了@Lookup注解的方法。由于其的返回类型是一个多例Bean。故每次调用该方法,都可以获取到不同的Bean实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.aaronzhu.SpringBoot1.PrototypeTest.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackPig;
import org.springframework.beans.factory.annotation.Lookup;
import org.springframework.stereotype.Service;

@Service
public class DService {

@Lookup
public BlackPig getPig() {
return null;
}

public void test() {
for (int i=0; i<5; i++) {
// 要获取Bean,就可以使用添加了@Lookup的方法来获取
BlackPig blackPig = getPig();
System.out.printf("[Pig Say]: %d -->> %s%n", i, blackPig);
}
}
}

测试用例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.aaronzhu.SpringBoot1.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.service.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PrototypeTest {

@Autowired
private DService dService;

@Test
public void test3() {
dService.test();
}

}

测试结果如下,符合预期

figure 3.png

ApplicationContext 应用上下文

由于Spring的Prototype原型Bean保证了每次通过 ApplicationContext 应用上下文 获取实例对象时都是不同的,故我们也可以直接通过 ApplicationContext 应用上下文来获取实例对象。首先,我们需要先实现ApplicationContextAware接口的setApplicationContext方法来获取到 ApplicationContext 应用上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.aaronzhu.SpringBoot1.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextUtil implements ApplicationContextAware {

private static ApplicationContext context = null;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}

public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}

然后,提供一个多例Bean的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.aaronzhu.SpringBoot1.PrototypeTest.pojo;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(value= ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class BlackTiger {
private String name;

private String sex;

}

现在,我们就可以在Service当中通过ApplicationContext应用上下文获取Bean了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.aaronzhu.SpringBoot1.PrototypeTest.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.pojo.BlackTiger;
import com.aaronzhu.SpringBoot1.util.ApplicationContextUtil;
import org.springframework.stereotype.Service;

@Service
public class EService {

public void test() {
for (int i=0; i<5; i++) {
// 通过ApplicationContext应用上下文获取Bean
BlackTiger blackTiger = ApplicationContextUtil.getBean(BlackTiger.class);
System.out.printf("[Tiger Say]: %d -->> %s%n", i, blackTiger);
}
}
}

测试用例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.aaronzhu.SpringBoot1.service;

import com.aaronzhu.SpringBoot1.PrototypeTest.service.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PrototypeTest {

@Autowired
private EService eService;

@Test
public void test4() {
eService.test();
}

}

测试结果符合预期

figure 4.png

请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝