三方依赖bean初始化导致项目启动失败问题

  现象:shua-video项目中引用了配置中台bp-config的SDK,然后在mq消息监听类中使用。如上使用方式,在waterService中引用了bp-config。在测试环境mq中没有消息消费时项目能正常启动,但在线上有消息消费时项目启动报错,提示找不到bp-config类。

@Component
@Slf4j
public class BpUserAfterWechatRegisterConsumer {

    public static final String CONSUMER_GROUP = "after_register_for_shua_farm";

    @Resource(name = CONSUMER_GROUP)
    private RocketMQBaseConsumer bpUserAfterWecahtRegister;

    @Autowired
    private WaterService waterService;

    @PostConstruct
    public void init() {
        new Thread(() -> {
            try {
                // {"createTime":1571904938893,"appId":3,"wechatId":4,"userId":1000000547}
                bpUserAfterWecahtRegister.setConsumerGroup(CONSUMER_GROUP);
                bpUserAfterWecahtRegister.setMessageModel(MessageModel.CLUSTERING);
                bpUserAfterWecahtRegister.subscribe(UserMQConstants.TOPIC_USER_AFTER_WECHAT_REGISTER);
                bpUserAfterWecahtRegister.registerMessageListener((msgs, context) -> {
                    for (MessageExt messageExt : msgs) {
                        try {
                          
                            //加新手引导水滴
                            waterService.addWaterForRegister(userId);

                        } catch (UnsupportedEncodingException e) {
                            log.error("消费消息出现异常", e);
                            continue;
                        }
                    }
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                });
                bpUserAfterWecahtRegister.start();
                log.info("bpUserAfterWecahtRegister 消费者启动完成");
            } catch (Exception e) {
                log.error("bp-user-consumer 初始化失败", e);
            }
        }).start();
    }

  问题分析:怀疑是spring类的初始化顺序问题

(1)首先看一下@PostConstruct注解的调用时机

  参考文章:https://www.cnblogs.com/lay2017/p/11735802.html

  发现@PostConstruct是在bean初始化时,由前置处理器processor.postProcessBeforeInitialization中通过反射去调用的,bean初始化是在DI注入之后进行的,而bean又是在使用时才首次从容器中getBean获取时才创建,所以没有消息时不会触发bp-config的创建,因此项目能正常启动。

(2)查看一下bp-config类加载时机

  在classpath下的spring.factoris文件中定义如下:

org.springframework.context.ApplicationListener=\
com.coohua.bp.config.api.spring.ConfigContainerBootListener

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.coohua.bp.config.api.spring.BpConfigAutoConfig

  ConfigContainerBootListener如下:

public class ConfigContainerBootListener implements ApplicationListener<ApplicationEvent> {

    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        if(applicationEvent instanceof ApplicationReadyEvent){
            GlobalConfigContainer globalConfigContainer = ConfigContainerHolder.getGlobalConfigContainer();
            globalConfigContainer.init();
            globalConfigContainer.start();

            StrategyConfigContainer strategyConfigContainer = ConfigContainerHolder.getStrategyConfigContainer();
            strategyConfigContainer.init();
            globalConfigContainer.start();
        }
    }
}

  可以看到,bp-config中类的初始化是依赖于spring的ApplicationReadyEvent事件的。

springboot支持的几类事件:

  1. ApplicationFailedEvent:该事件为spring boot启动失败时的操作

  2. ApplicationPreparedEvent:上下文context准备时触发

  3. ApplicationReadyEvent:上下文已经准备完毕的时候触发

  4. ApplicationStartedEvent:spring boot 启动监听类

  5. SpringApplicationEvent:获取SpringApplication

  6. ApplicationEnvironmentPreparedEvent:环境事先准备

(3)看一下springBoot中的IOC容器初始化顺序

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context); //容器初始化,PostConstruct就是在这里执行的
            afterRefresh(context, applicationArguments);  //ApplicationReadyEvent事件发布时机
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }

  从上面注释可以看出,spring在refresh初始化IOC容器时,bp-config还未来得及初始化,所以此时在@PostConstruct中调用bp-config会有提示找不到。

  springBoot启动流程分析:https://blog.csdn.net/yhahaha_/article/details/89152280

(4)为了快速解决项目上线问题,本项目中采用如下方式解决

@Service
@Slf4j
public class OrderPaySuccessConsumerForShuaVideo implements ApplicationListener<ApplicationReadyEvent>{
    public final static String ORDER_PAY_SUCCESS_FOR_SHUA_VIDEO = "order_pay_success_for_shua_video";

    @Resource(name = ORDER_PAY_SUCCESS_FOR_SHUA_VIDEO)
    private RocketMQBaseConsumer orderPaySuccessForShuaVideo;

    //@PostConstruct
    private void runConsumer() {
        log.info("init {} consumer", ORDER_PAY_SUCCESS_FOR_SHUA_VIDEO);
        try {
            orderPaySuccessForShuaVideo.subscribe(MallMQConstants.TOPIC_ORDER_PAY_SUCCESS);
            orderPaySuccessForShuaVideo.setConsumerGroup(ORDER_PAY_SUCCESS_FOR_SHUA_VIDEO);
            orderPaySuccessForShuaVideo.registerMessageListener(new MessageListenerConcurrently() {
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                    for (MessageExt messageExt : list) {
                        try {
                                        userFriendService.handleDividendValidFriend(userId,true);
                                    }
                                } catch (Exception e) {
                                    log.warn("解构失败: {}", orderInfo, e);
                                }
                            }
                        } catch (Exception e) {
                            log.error("不支持的编码集。", e);
                        }
                    }
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            orderPaySuccessForShuaVideo.start();
        } catch (Exception e) {
            log.error("消费者: {} 启动异常!", ORDER_PAY_SUCCESS_FOR_SHUA_VIDEO, e);
        }
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException e) {
                    runConsumer();
                }
                runConsumer();
            }
        });
    }
}

  方式就是在bp-config初始化完成后再进行消息消费,同样也监听ApplicationReadyEvent事件,上线后问题解决。

  这里有一个知识点,就是监听器的注册时机:

    *  bp-config中的监听器是在spring.factories中定义的,在springboot的run方法中通过getRunListeners()注册。

    *  类级别自定义的监听器是在refresh方法中通过registerListeners()注册的,所以前者先注册会先执行。

(5)修改

  这里依赖spring事件初始化对业务方有一定的侵入,所以应解除spring的依赖,采用原生方式(延迟初始化),这其实也是spring中bean的初始化思路(实例化在getBean时进行)。

public static String getConfig(Long appId, String key) {
        if (appId == null || StringUtils.isEmpty(key)) {
            throw new NullPointerException("appId or key is null");
        }
        Map<String, String> config = ConfigContainerHolder.getGlobalConfigContainer().getConfig(appId);
        if (config == null) {
            return null;
        }
        return config.get(key);
    }
public static GlobalConfigContainer getGlobalConfigContainer() {
        if (!initGlobalConfig) {
            globalConfigLock.lock();
            try {
                if (!initGlobalConfig) {
                    GlobalConfigContainer globalConfigContainer = new GlobalConfigContainer();
                    globalConfigContainer.init();
                    globalConfigContainer.start();
                    containerMap.put(GlobalConfigContainer.CONTAINER_NAME, globalConfigContainer);
                    initGlobalConfig = true;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                globalConfigLock.unlock();
            }
        }
        return (GlobalConfigContainer) containerMap.get(GlobalConfigContainer.CONTAINER_NAME);
    }

  这也是在开发公共组件时要注意的地方,尽量减少对业务方的侵入。

 

posted @ 2021-08-16 10:37  jingyi_up  阅读(365)  评论(0编辑  收藏  举报