Spring中的Bean除了前面提到的幾種JavaConfig或者@Component
等註解標識之外,也是可以動態的向Spring容器註冊的,本篇博文將主要介紹
- 如何向Spring容器註冊Bean
- 如何引用主動註冊的Bean
- 註冊的Bean中,如果依賴其他的Bean,怎麼操作
I. 手動註冊Bean方式
1. 核心實現類
以前也寫過關於動態註冊Bean的博文,如 180804-Spring之動態註冊bean
我們的實現方式和上面也沒什麼區別,依然是藉助BeanDefinition
來建立Bean定義並註冊到BeanFactory中,具體實現的核心程式碼如下
public class ManualRegistBeanUtil {
/**
* 主動向Spring容器中註冊bean
*
* @param applicationContext Spring容器
* @param name BeanName
* @param clazz 註冊的bean的類性
* @param args 構造方法的必要引數,順序和型別要求和clazz中定義的一致
* @param <T>
* @return 返回註冊到容器中的bean物件
*/
public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String name, Class<T> clazz,
Object... args) {
if(applicationContext.containsBean(name)) {
Object bean = applicationContext.getBean(name);
if (bean.getClass().isAssignableFrom(clazz)) {
return (T) bean;
} else {
throw new RuntimeException("BeanName 重複 " + name);
}
}
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
for (Object arg : args) {
beanDefinitionBuilder.addConstructorArgValue(arg);
}
BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
beanFactory.registerBeanDefinition(name, beanDefinition);
return applicationContext.getBean(name, clazz);
}
}
複製程式碼
上面唯一的方法中,接收四個引數,原始碼中也有說明,稍微需要注意下的是Spring容器中不允許出現同名的Bean
2. 測試用例
動態建立Bean,並不是塞入容器之中就完結了,塞進去之後,是為了後續的使用,自然而然的就會有下面幾種情形
a. 無其他Bean依賴
即不依賴其他的Bean, 單純的供其他地方使用,這種情況下,主要需要測試的就是別人可以通過什麼方式來使用它
@Slf4j
public class ManualBean {
private int id;
public ManualBean() {
Random random = new Random();
id = random.nextInt(100);
}
public String print(String msg) {
return "[ManualBean] print : " + msg + " id: " + id;
}
}
複製程式碼
b. 依賴其他Bean
和前面一個不同,這個Bean內部需要注入其他的Bean,因此我們主動註冊Bean時,能否將依賴的Bean也注入進去呢?
定義一個測試Bean
@Slf4j
public class ManualDIBean {
private int id;
@Autowired
private OriginBean originBean;
private String name;
public ManualDIBean(String name) {
Random random = new Random();
this.id = random.nextInt(100);
this.name = name;
}
public String print(String msg) {
String o = originBean.print(" call by ManualDIBean! ");
return "[ManualDIBean] print: " + msg + " id: " + id + " name: " + name + " originBean print:" + o;
}
}
複製程式碼
其依賴的普通Bean定義如下
@Slf4j
@Component
public class OriginBean {
private LocalDateTime time;
public OriginBean() {
time = LocalDateTime.now();
}
public String print(String msg) {
return "[OriginBean] print msg: " + msg + ", time: " + time;
}
}
複製程式碼
c. 普通Bean依賴主動註冊的Bean
這個其實就是使用case了,主動註冊的Bean也是被人使用的,那可以怎麼使用呢?傳統的Autowired
可否?
@Slf4j
@Component
public class AnoOriginBean {
// 希望可以注入 主動註冊的Bean
@Autowired
private ManualBean manualBean;
public AnoOriginBean() {
System.out.println("AnoOriginBean init: " + System.currentTimeMillis());
}
public String print() {
return "[AnoOriginBean] print!!! manualBean == null ? " + (manualBean == null);
}
}
複製程式碼
d. Bean註冊實現
前面定義了兩個需要手動註冊的bean,所以就需要選擇一個合適的地方來處理主動註冊的邏輯,我們把這段邏輯放在AutoConfig中,用於測試演示
@Configuration
public class BeanRegisterAutoConf {
public BeanRegisterAutoConf(ApplicationContext applicationContext) {
System.out.println("BeanRegisterAutoConf init: " + System.currentTimeMillis());
registerManualBean((ConfigurableApplicationContext) applicationContext);
}
/**
* 手動註冊自定義地bean
* @param applicationContext
*/
private void registerManualBean(ConfigurableApplicationContext applicationContext) {
// 主動註冊一個沒什麼依賴的Bean
ManualBean manualBean = ManualRegistBeanUtil.registerBean(applicationContext, "manualBean", ManualBean.class);
manualBean.print("test print manualBean");
// manualDIBean 內部,依賴由Spring容器建立的OriginBean
ManualDIBean manualDIBean = ManualRegistBeanUtil.registerBean(applicationContext, "manualDIBean",
ManualDIBean.class, "依賴OriginBean的自定義Bean");
manualDIBean.print("test print manualDIBean");
}
}
複製程式碼
3. 實測演示
前面的測試case都準備好了,接著就需要實際的跑一下看看效果了,選擇Rest服務來演示,建立一個簡單的Controller
@RestController
public class ShowController {
@Autowired
private ManualBean manualBean;
@Autowired
private ManualDIBean manualDIBean;
@Autowired
private AnoOriginBean anoOriginBean;
public ShowController() {
System.out.println("ShowController init: " + System.currentTimeMillis());
}
@GetMapping(path = "show")
public String show(String msg) {
Map<String, String> result = new HashMap<>(8);
result.put("manualBean", manualBean == null ? "null" : manualBean.print(msg));
result.put("manualDIBean", manualDIBean == null ? "null" : manualDIBean.print(msg));
result.put("anoOriginBean",anoOriginBean.print());
return JSONObject.toJSONString(result);
}
}
複製程式碼
上面就使用了三個Bean,兩個主動註冊的外加一個依賴了主動註冊Bean的anoOriginBean
(其實Controller本身也是一個使用主動註冊Bean的Bean)
先預測一下結果:
- 如果 manualBean, manualDIBean 為空,表示不能直接通過
@Autowired
註解的方式引入手動註冊的Bean;此時會拋npe - 如果沒有npe,且 AnoOriginBean內部依賴的manualBean也不是null,則表示直接用
@Autowired
來注入沒啥毛病(是否絕對呢?) - manualDIBean 內部依賴了
originBean
,也是通過註解方式注入,如果正常返回,表示手動註冊的也可以這麼引用其他的Bean;否則不行
執行結果如上圖,簡單來說,就是手動註冊的Bean,和我們一般使用的Bean也沒什麼兩樣,原來可以怎麼用,現在依然可以這麼用
II. BeanDefinitionRegistryPostProcessor擴充套件方式
前面這種手動注入的方式有個不好的地方就是主動註冊的這個邏輯,感覺寫在什麼地方都不太優雅,在Spring專案的原始碼中通過實現BeanDefinitionRegistryPostProcessor擴充套件方式
介面的方式比較多,比如org.springframework.cloud.autoconfigure.RefreshAutoConfiguration
依葫蘆畫瓢實現一個
1. 實現類
@Slf4j
@Configuration
public class AutoBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 註冊Bean定義,容器根據定義返回bean
//構造bean定義
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.genericBeanDefinition(AutoBean.class);
BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
//註冊bean定義
registry.registerBeanDefinition("autoBean", beanDefinition);
// AutoDIBean 的注入方式
beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(AutoDIBean.class);
beanDefinitionBuilder.addConstructorArgValue("自動注入依賴Bean");
beanDefinition = beanDefinitionBuilder.getBeanDefinition();
registry.registerBeanDefinition("autoDiBean", beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
// 註冊Bean例項,使用supply介面, 可以建立一個例項,並主動注入一些依賴的Bean;當這個例項物件是通過動態代理這種框架生成時,就比較有用了
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(AutoFacDIBean.class, () -> {
AutoFacDIBean autoFacDIBean = new AutoFacDIBean("autoFac");
autoFacDIBean.setAutoBean(factory.getBean("autoBean", AutoBean.class));
autoFacDIBean.setOriginBean(factory.getBean("originBean", OriginBean.class));
return autoFacDIBean;
});
BeanDefinition beanDefinition = builder.getRawBeanDefinition();
((DefaultListableBeanFactory) factory).registerBeanDefinition("autoFacDIBean", beanDefinition);
}
}
複製程式碼
介面的實現中,Bean的註冊方式和前面的其實是一樣的,這個介面提供了兩個方法,通常實現第一個方法來做Bean的註冊;兩者從根本上也沒太大的區別,上面只是給出了一種使用演示
2. 測試用例
測試的思路基本上和前面一樣,定義了三個需要我們註冊的Bean,一個沒有外部依賴的AutoBean
public class AutoBean {
public String print() {
return "[AutoBean] " + System.currentTimeMillis();
}
}
複製程式碼
一個依賴外部Bean的AutoDIBean
public class AutoDIBean {
private String name;
@Autowired
private OriginBean originBean;
public AutoDIBean(String name) {
this.name = name;
}
public String print() {
return "[AutoDIBean] " + name + " originBean == null ? " + (originBean == null);
}
}
複製程式碼
一個用於主動建立和設定依賴的AutoFacDIBean
(用於前面的實現類中的第二個方法的註冊方式)
public class AutoFacDIBean {
private String name;
@Setter
private OriginBean originBean;
@Setter
private AutoBean autoBean;
public AutoFacDIBean(String name) {
this.name = name;
}
public String print() {
return "[AutoDIBean] " + name + " originBean == null ? " + (originBean == null) + " | autoBean==null ? " +
(autoBean == null);
}
}
複製程式碼
一個依賴了主動註冊AutoBean的 AnoAutoOriginBean
@Component
public class AnoAutoOriginBean {
@Autowired
private AutoBean autoBean;
public AnoAutoOriginBean() {
System.out.println("AnoAutoOriginBean init: " + System.currentTimeMillis());
}
public String print() {
return "[AnoAutoOriginBean] print!!! autoBean == null ? " + (autoBean == null);
}
}
複製程式碼
3. 實測演示
同樣寫一個RestApi進行演示,通過實際的演示結果發現和前面沒什麼太大的區別
@Autowired
private AutoBean autoBean;
@Autowired
private AutoDIBean autoDIBean;
@Autowired
private AutoFacDIBean autoFacDIBean;
@Autowired
private AnoAutoOriginBean anoAutoOriginBean;
@GetMapping(path = "auto")
public String autoShow() {
Map<String, String> result = new HashMap<>(8);
result.put("autoBean", autoBean == null ? "null" : autoBean.print());
result.put("manualDIBean", autoDIBean == null ? "null" : autoDIBean.print());
result.put("autoFacDIBean",autoFacDIBean == null ? "null" : autoFacDIBean.print());
result.put("anoAutoOriginBean",anoAutoOriginBean.print());
return JSONObject.toJSONString(result);
}
複製程式碼
III. 其他
0. 相關
a. 文件
b. 原始碼
- 工程:spring-boot-demo
- model: 006-dynamicbean
1. 一灰灰Blog
- 一灰灰Blog個人部落格 blog.hhui.top
- 一灰灰Blog-Spring專題部落格 spring.hhui.top
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
3. 掃描關注
一灰灰blog