SpringBoot基礎篇Bean之動態註冊

一灰灰發表於2018-10-21

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. 原始碼


1. 一灰灰Blog

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 宣告

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

一灰灰blog

QrCode

相關文章