三方依赖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支持的几类事件:
-
ApplicationFailedEvent:该事件为spring boot启动失败时的操作
-
ApplicationPreparedEvent:上下文context准备时触发
-
ApplicationReadyEvent:上下文已经准备完毕的时候触发
-
ApplicationStartedEvent:spring boot 启动监听类
-
SpringApplicationEvent:获取SpringApplication
-
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); }
这也是在开发公共组件时要注意的地方,尽量减少对业务方的侵入。