二方庫開發過程當中防止bean衝突的思考

咱們開發內部用的二方庫時每每須要定義一些bean,這些bean中有的可能已經被業務方系統配置使用了,在非SpringBoot方式集成中可能致使衝突。致使按type注入失敗(由於存在兩個已有的實現)。爲何要強調非SpringBoot呢,由於SpringBoot可使用@ConditionOnMissingXxx條件註解。html

如何解決非SpringBoot集成狀況下可能存在衝突的問題,有如下三種方案:
一、強制業務系統集成出現衝突時使用@Qualifier標明其本身已存在的衝突bean,以防止按type注入出現的衝突異常。而咱們本身的二方庫也得加上@Qualifier,可是咱們須要同時提供普通Spring集成和SpringBoot集成方式,那麼加上@Qualifier又會成爲咱們本身給本身設置的麻煩,由於在存在@ConditionOnMissingBean的時候,咱們極有可能沿用業務系統配置的bean,而此時咱們是不知道其id的。從而致使@Qualifier失去了意義。java

二、使用@Primary標註咱們二方庫裏的bean爲首選項,這樣對於業務系統而言就會出現太過透明而沒法知曉到底注入了它本身的仍是二方庫裏的,並且若是業務系統存在特殊配置呢就被咱們覆蓋了。還有種方式是強制業務系統標註本身的bean爲@Primary。spring

三、採用動態加載裝配bean定義的方式。若是已存在則不裝配,不存在才進行裝配。效果相似於@ConditionMissingBeanapp

因爲第三種,對於集成方更爲友好,因此採用第三種方案去實施。
主體思路:在Spring裝配完全部的bean以後才實施咱們的動態裝配邏輯。ide

Spring的bean生命週期中的一些鉤子

Spring提供了不少接口用於咱們在bean初始化先後實現自定義邏輯,目前接觸較多的有:BeanFactoryAware,ApplicationContextAware,ApplicationListener,InitializingBean,BeanPostProcessor。
針對這幾個接口,咱們梳理下bean初始化執行順序:
bean自己的構造器初始化調用->BeanPostProcessor的前置處理調用postProcessBeforeInitialization->InitializingBean的afterPropertiesSet調用->BeanPostProcessor的後置處理調用postProcessAfterInitialization
要驗證以上順序也不難,能夠寫幾個實現,打印一些日誌,觀察先後輸出,去測試驗證一下,簡要截圖以下:spring-boot

clipboard.png

注意:其中MyBean4是BeanPostProcessor的實現類。post

這隻討論了單個bean的一個執行順序,那麼全部bean是哪一個時候執行完的呢,或者說回調那個接口方法時,全部已經初始化完成並已徹底就緒呢?咱們來分析下這三個接口BeanFactoryAware,ApplicationContextAware,ApplicationListener
BeanFactoryAware,ApplicationContextAware這兩個咱們通常用來獲取並保持容器上下文,剛開始我在想,是否是在setBeanFactory或者setApplicationContext的時候,全部bean已經就緒呢?測試

clipboard.png

從截圖來看,很遺憾,並無如咱們想象般那樣。
接下來咱們關注一下這個ApplicationListener。Spring爲咱們提供瞭如下幾個事件以便於咱們對容器生命週期的監聽:this

  • ContextRefreshedEvent:This event is published whenever the Spring Context is started or refreshed.
  • ContextStartedEvent:This event is published when the Spring Context is started.
  • ContextStoppedEvent:This event is published when the Spring Context is stopped. In practice you will not use this event very often. It can be handy for doing cleanup work, like closing connections.
  • ContextClosedEvent:This event is similar to the ContextStoppedEvent, but in this case the Context can not be re-started.

咱們的關注點天然落在了ContextRefreshedEvent及ContextStartedEvent,可是按說服,彷佛ContextRefrehedEvent更符合咱們的需求,咱們驗證一下,是否該事件是在全部bean都就緒以後纔會發出。spa

clipboard.png

從日誌中咱們能夠看到,在打印了onApplicationEvent日誌以後,沒有再出現beanPostProcessor的日誌,說明確實是在全部bean都初始化完成以後才調用。

上述需求的動態裝配實現代碼以下:

public class SpringListener implements ApplicationListener<ContextRefreshedEvent> {

    private static final Logger log=LoggerFactory.getLogger(SpringListener.class);

    private  static ApplicationContext applicationContext;


    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {

        log.info("bizrule dynamic bean load start...");
        applicationContext=contextRefreshedEvent.getApplicationContext();

        XRuleEngine xRuleEngine=applicationContext.getBean(XRuleEngine.class);

        try {
            ConfigProps configProps=new ConfigProps(xRuleEngine);

    
            DynamicBeanLoader.dynamicLoadBizBean(applicationContext,EmployeeBasicService.class,
                    configProps.getMasterDataVersion(),"",Integer.valueOf(configProps.getMasterDataTimeout()+""));

            DynamicBeanLoader.dynamicLoadBizBean(applicationContext,EmployeeJobLevelService.class,
                    configProps.getMasterDataVersion(),"",Integer.valueOf(configProps.getMasterDataTimeout()+""));

            DynamicBeanLoader.dynamicLoadBizBean(applicationContext,EmployeeDeptService.class,
                    configProps.getMasterDataVersion(),"",Integer.valueOf(configProps.getMasterDataTimeout()+""));

   
            DynamicBeanLoader.dynamicLoadBean(applicationContext,MasterdataServiceImpl.class,
                Arrays.asList("secret::"+configProps.getMasterDataSecret(),"clientId::"+configProps.getMasterDataClientId())
                    .stream().map(v->v.split("::")).collect(Collectors.toMap(v->v[0],v->v[1])));

            MasterdataService masterdataService=applicationContext.getBean(MasterdataService.class);

            DynamicBeanLoader.dynamicLoadBean(applicationContext,RcUserServiceImpl.class,new HashMap<String,Object>());

            RcUserService rcUserService=applicationContext.getBean(RcUserService.class);

            log.info("start autowired bizrule plugin beans");
            BizAmountDecisionService bizAmountDecisionService= null;
            try {
                bizAmountDecisionService = applicationContext.getBean(BizAmountDecisionService.class);
                bizAmountDecisionService.setRcUserService(rcUserService);
                log.info("autowired beans for bizAmountDecisionService");
            } catch (NoSuchBeanDefinitionException e) {
                log.info("didn't find BizAmountDecisionService bean, ignore it's RcUserService autowired. ");
            }

            UserReportLineService userReportLineService= null;
            try {
                userReportLineService = applicationContext.getBean(UserReportLineService.class);
                userReportLineService.setRcUserService(rcUserService);
                log.info("autowired beans for userReportLineService");
            } catch (NoSuchBeanDefinitionException e) {
                log.info("didn't find UserReportLineService bean, ignore it's RcUserService autowired. ");
            }
            log.info("dynamic bizrule common bean loader over");
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage(),e);
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static void setApplicationContext(ApplicationContext applicationContext) {
        SpringListener.applicationContext = applicationContext;
    }
}
public class DynamicBeanLoader {

    private static final Logger log=LoggerFactory.getLogger(DynamicBeanLoader.class);

    @Deprecated
    public static void dynamicLoadBean(ApplicationContext context,Resource... resources) {
        AutowireCapableBeanFactory factory = context.getAutowireCapableBeanFactory();
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) factory;
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(
            registry);
        beanDefinitionReader.setResourceLoader(context);
        beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(context));
        try {
            for (int i = 0; i < resources.length; i++) {
                beanDefinitionReader.loadBeanDefinitions(resources[i]);
            }
        } catch (BeansException e) {
            e.printStackTrace();
            log.error(e.getMessage(),e);
        }
    }
    public static void dynamicLoadBean(ApplicationContext context,Class clazz,Map<String,Object> params){
        if(isBeanLoaded(context,clazz)){
            return;
        }
        AutowireCapableBeanFactory autowireCapableBeanFactory = context.getAutowireCapableBeanFactory();
        DefaultListableBeanFactory beanFactory=(DefaultListableBeanFactory)autowireCapableBeanFactory;
        GenericBeanDefinition gbd = new GenericBeanDefinition();
        gbd.setBeanClass(clazz);
        MutablePropertyValues mpv = new MutablePropertyValues();
        params.entrySet().stream().forEach(e->{
            mpv.add(e.getKey(), e.getValue());
        });
        gbd.setPropertyValues(mpv);
        beanFactory.registerBeanDefinition("xc"+clazz.getSimpleName(), gbd);
    }
    public static void dynamicLoadBizBean(ApplicationContext context,Class clazz,String version,String group,int timeout){
        //略。。。。
    }

    public static boolean isBeanLoaded(ApplicationContext context,Class clazz){
        //之因此採用捕獲異常而不是containsBean的形式,是由於containsBean僅支持按名稱判斷,在這種場景下有侷限性。
        try {
            context.getBean(clazz);
            log.info("find {} in system, ignore load",clazz.getName());
            return true;
        } catch (NoSuchBeanDefinitionException e) {
            log.info("not find {} in system, will being load",clazz.getName());
            return false;
        }
    }

}

SpringBoot Starter開發

咱們同時提供SpringBoot starter集成。
先貼上官方文檔:https://docs.spring.io/spring...
官方文檔寫得很簡單很抽象。不過提供了思路,SpringBoot啓動的時候會去尋找META-INF/spring.factories文件,去加載其中的配置進行相應的初始化。默認配置由SpringBoot Autoconfigure模塊提供,注意到其中的片斷:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\

這裏定義了自動配置的類,咱們可找到對應的類看看實現。

而後咱們能夠隨便挑幾個starter看看,總結下思路步驟
一、定義AutoConfiguration類
二、若是有須要在application.properties加些配置,能夠定義XxxProperties類
三、定義META-INF/spring.factories文件

具體照着代碼說明吧:
pom依賴添加,這裏版本就不列出來了。

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

XxxProperties類定義:

@ConfigurationProperties(prefix = "xrulecenter.biz")
public class AmountDesionProperties {
}

這樣咱們就能夠在application.properties中使用   xrulecenter.biz.屬性=value 來配置

AutoConfiguration類定義

@Configuration
@EnableConfigurationProperties(value = {GetAuditorProperties.class})
@ConditionalOnClass(BizAuditorServiceImpl.class)
@ConditionalOnProperty(prefix = "xrulecenter.bizrule.enabled",name={"getauditor"},havingValue = "true",matchIfMissing = true)
public class GetAuditorAutoConfiguration {

    @Autowired
    private GetAuditorProperties getAuditorProperties;


    @Bean
    @ConditionalOnMissingBean
    public SpringListener xcBizRuleSpringListener(){
        return new SpringListener();
    }

    @Bean
    @ConditionalOnMissingBean
    public BizAuditorService bizAuditorService(){
        return new BizAuditorServiceImpl();
    }
    @Bean
    @ConditionalOnMissingBean
    public UserReportLineService xcUserReportLineService(){
        return new UserReportLineServiceImpl();
    }
}

這裏有幾個註解須要注意一下:
@EnableConfigurationProperties使咱們前面定義的properties能生效
@ConditionalOnXxx 條件注入生效註解,其中@ConditionalOnProperty須要關注一下:經過它咱們能夠配置本身starter模塊的開關配置,而不是引入依賴即開啓,讓用戶有選擇地打開和關閉。
havingValue = "true",matchIfMissing = true這兩個屬性同時添加的意思是:若是用戶在application.properties配置了xrulecenter.bizrule.enabled=true或者沒有此配置都將使該starter生效。

定義spring.factories文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.auditor.GetAuditorAutoConfiguration

這樣咱們的starter就完工了。

相關文章
相關標籤/搜索