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

楔子
通常在使用Spring时Bean都是单例的,但有时候我们也会有需要使用多例的场景。为此Spring提供了Prototype原型Bean。其会在每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例
众所周知,在Spring中使用@Component、@Service等注解创建的Bean默认都是单例的。如果想使用多例也很简单,只需使用@Scope注解,并将value属性设置为prototype即可
下面我们分别提供一个单例Bean、原型Bean示例
| 12
 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;
 }
 
 | 
| 12
 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来验证下效果
| 12
 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);
 }
 }
 }
 
 | 
| 12
 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);
 }
 }
 }
 
 | 
测试用例如下
| 12
 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实例中,每次使用的是却是同一个实例对象

解决方案
问题来了,不是说Prototype原型Bean每次都会使用一个新的实例对象么?为什么在同一个Service实例中,每次使用的是却是同一个实例对象呢?原因在于Spring的多例是指在每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的Bean实例。显然对于某一个Service实例而言,在注入多例Bean的属性时,Spring会创建一个全新的多例Bean进行注入。但一旦完成属性注入后,该Service实例其所使用的多例Bean即会被固定住,后续将不会再发生更改。那有没有办法实现,即使在同一个Service实例当中,每次使用的多例Bean都是不同的对象呢?答案自然是有的
@Scope注解的proxyMode属性
最简单的办法就是通过代理对象,设置@Scope注解的proxyMode属性为 ScopedProxyMode.TARGET_CLASS 即可。下述是一个设置proxyMode属性的多例Bean示例
| 12
 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
| 12
 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);
 }
 }
 }
 
 | 
测试用例如下所示
| 12
 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();
 }
 
 }
 
 | 
测试结果如下所示,符合预期

@Lookup注解
我们可以将@Lookup注解在一个非静态的方法上,这样每次调用这个方法时Spring会动态地查找、注入相应的Bean实例。当我们不设置@Lookup注解的value属性时,其会根据方法签名中的返回类型去返回相应类型的Bean实例。与此同时,由于该方法的方法体并不会被真正执行,故我们直接return null,也不会影响结果获取。我们先定义一个多例Bean示例
| 12
 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实例
| 12
 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++) {
 
 BlackPig blackPig = getPig();
 System.out.printf("[Pig Say]: %d -->> %s%n", i, blackPig);
 }
 }
 }
 
 | 
测试用例如下
| 12
 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();
 }
 
 }
 
 | 
测试结果如下,符合预期

ApplicationContext 应用上下文
由于Spring的Prototype原型Bean保证了每次通过 ApplicationContext 应用上下文 获取实例对象时都是不同的,故我们也可以直接通过 ApplicationContext 应用上下文来获取实例对象。首先,我们需要先实现ApplicationContextAware接口的setApplicationContext方法来获取到 ApplicationContext 应用上下文
| 12
 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的定义
| 12
 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了
| 12
 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++) {
 
 BlackTiger blackTiger = ApplicationContextUtil.getBean(BlackTiger.class);
 System.out.printf("[Tiger Say]: %d -->> %s%n", i, blackTiger);
 }
 }
 }
 
 | 
测试用例如下
| 12
 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();
 }
 
 }
 
 | 
测试结果符合预期
