二方库开发过程中防止bean冲突的思考

我们开发内部用的二方库时往往需要定义一些bean,这些bean中有的可能已经被业务方系统配置使用了,在非SpringBoot方式集成中可能导致冲突。导致按type注入失败(因为存在两个已有的实现)。为什么要强调非SpringBoot呢,因为SpringBoot可以使用@ConditionOnMissingXxx条件注解。

如何解决非SpringBoot集成情况下可能存在冲突的问题,有以下三种方案:
1、强制业务系统集成出现冲突时使用@Qualifier标明其自己已存在的冲突bean,以防止按type注入出现的冲突异常。而我们自己的二方库也得加上@Qualifier,但是我们需要同时提供普通Spring集成和SpringBoot集成方式,那么加上@Qualifier又会成为我们自己给自己设置的麻烦,因为在存在@ConditionOnMissingBean的时候,我们极有可能沿用业务系统配置的bean,而此时我们是不知道其id的。从而导致@Qualifier失去了意义。

2、使用@Primary标注我们二方库里的bean为首选项,这样对于业务系统而言就会出现太过透明而无法知晓到底注入了它自己的还是二方库里的,而且如果业务系统存在特殊配置呢就被我们覆盖了。还有种方式是强制业务系统标注自己的bean为@Primary。

3、采用动态加载装配bean定义的方式。如果已存在则不装配,不存在才进行装配。效果类似于@ConditionMissingBean

由于第三种,对于集成方更为友好,所以采用第三种方案去实施。
主体思路:在Spring装配完所有的bean之后才实施我们的动态装配逻辑。

Spring的bean生命周期中的一些钩子

Spring提供了很多接口用于我们在bean初始化前后实现自定义逻辑,目前接触较多的有:BeanFactoryAware,ApplicationContextAware,ApplicationListener,InitializingBean,BeanPostProcessor。
针对这几个接口,我们梳理下bean初始化执行顺序:
bean本身的构造器初始化调用->BeanPostProcessor的前置处理调用postProcessBeforeInitialization->InitializingBean的afterPropertiesSet调用->BeanPostProcessor的后置处理调用postProcessAfterInitialization
要验证以上顺序也不难,可以写几个实现,打印一些日志,观察前后输出,去测试验证一下,简要截图如下:

《二方库开发过程中防止bean冲突的思考》

注意:其中MyBean4是BeanPostProcessor的实现类。

这只讨论了单个bean的一个执行顺序,那么所有bean是哪个时候执行完的呢,或者说回调那个接口方法时,所有已经初始化完成并已完全就绪呢?我们来分析下这三个接口BeanFactoryAware,ApplicationContextAware,ApplicationListener
BeanFactoryAware,ApplicationContextAware这两个我们一般用来获取并保持容器上下文,刚开始我在想,是不是在setBeanFactory或者setApplicationContext的时候,所有bean已经就绪呢?

《二方库开发过程中防止bean冲突的思考》

从截图来看,很遗憾,并没有如我们想象般那样。
接下来我们关注一下这个ApplicationListener。Spring为我们提供了以下几个事件以便于我们对容器生命周期的监听:

  • 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都就绪之后才会发出。

《二方库开发过程中防止bean冲突的思考》

从日志中我们可以看到,在打印了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看看,总结下思路步骤
1、定义AutoConfiguration类
2、如果有需要在application.properties加些配置,可以定义XxxProperties类
3、定义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就完工了。

    原文作者:xbynet
    原文地址: https://segmentfault.com/a/1190000015113548
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞