spring相关的问题和原因分析

摘要:
如上使用方式,在waterService中引用了bp-config。在测试环境mq中没有消息消费时项目能正常启动,但在线上有消息消费时项目启动报错,提示找不到bp-config类。

1、Bean的初始化顺序导致的项目启动失败

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

@Component
@Slf4j
public classBpUserAfterWechatRegisterConsumer {

    public static final String CONSUMER_GROUP = "after_register_for_shua_farm";

    @Resource(name =CONSUMER_GROUP)
    privateRocketMQBaseConsumer bpUserAfterWecahtRegister;

    @Autowired
    privateWaterService waterService;

    @PostConstruct
    public voidinit() {
        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;
                        }
                    }
                    returnConsumeConcurrentlyStatus.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 voidonApplicationEvent(ApplicationEvent applicationEvent) {
        if(applicationEvent instanceofApplicationReadyEvent){
            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容器初始化顺序

publicConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = newStopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
//获取spring.factories中定义的监听器 SpringApplicationRunListeners listeners
=getRunListeners(args); listeners.starting(); try{ ApplicationArguments applicationArguments = newDefaultApplicationArguments( 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); //初始化IOC容器 refreshContext(context); afterRefresh(context, applicationArguments); 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 newIllegalStateException(ex); } try{ listeners.running(context); } catch(Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw newIllegalStateException(ex); } return context;

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

(4)联系配置中台开发同学,告诉此处依赖spring事件初始化的不合理之处

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

@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)
    privateRocketMQBaseConsumer orderPaySuccessForShuaVideo;

    //@PostConstruct
    private voidrunConsumer() {
        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(newMessageListenerConcurrently() {
                @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);
                        }
                    }
                    returnConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            orderPaySuccessForShuaVideo.start();
        } catch(Exception e) {
            log.error("消费者: {} 启动异常!", ORDER_PAY_SUCCESS_FOR_SHUA_VIDEO, e);
        }
    }

    @Override
    public voidonApplicationEvent(ApplicationReadyEvent event) {
        Executors.newSingleThreadExecutor().submit(newRunnable() {
            @Override
            public voidrun() {
                try{
                    TimeUnit.SECONDS.sleep(30);
                } catch(InterruptedException e) {
                    runConsumer();
                }
                runConsumer();
            }
        });
    }
}

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

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

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

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

2、spring如何解决循环依赖

spring是如何检查有没有循环依赖的:

  当类被创建时会将对应的beanName放入Set<String>singletonsCurrentlyInCreation集合中,如何发现要创建的对象在集合中已经存在,说明存在循环依赖。

Spring中循环依赖场景有:

(1)构造器的循环依赖

这种循环依赖spring本身没法解决,只能通过代码层面:

*使用注解 @Lazy

@Component
public classCircularDependencyA {
 
    privateCircularDependencyB circB;
 
    publicCircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB =circB;
    }
}

使用延迟加载,在注入依赖时,先注入代理对象,在首次使用时再创建对象完成注入。

*使用Setter注入——官方建议

@Component
public classCircularDependencyA {
 
    privateCircularDependencyB circB;
 
    public voidsetCircB(CircularDependencyB circB) {
        this.circB =circB;
    }
 
    publicCircularDependencyB getCircB() {
        returncircB;
    }
public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

当依赖被使用时才进行注入。

(2)属性的循环依赖

https://juejin.im/post/5dbb9fdef265da4d4c202483(讲的很透彻)

3、spring中使用的设计模式
  • 工厂模式: spring中的BeanFactory就是简单工厂模式的体现,根据传入唯一的标识来获得bean对象;
  • 单例模式: 提供了全局的访问点BeanFactory;
  • 代理模式: AOP功能的原理就使用代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)
  • 装饰器模式: 依赖注入就需要使用BeanWrapper;
  • 观察者模式: spring中Observer模式常用的地方是listener的实现。如ApplicationListener。
  • 策略模式: Bean的实例化的时候决定采用何种方式初始化bean实例(反射或者CGLIB动态字节码生成)

4、spring IOC初始化的整个流程

https://juejin.im/post/5be976a76fb9a049fd0f5f31

免责声明:文章转载自《spring相关的问题和原因分析》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇CI框架3.x 之文件上传与生成缩略图数据库之索引与慢查询优化下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

c/c++中变量的作用域

作用域规则告诉我们一个变量的有效范围,它在哪儿创建,在哪儿销毁(也就是说超出了作用域)。变量的有效作用域从它的定义点开始,到和定义变量之前最邻近的开括号配对的第一个闭括号。也就是说,作用域由变量所在的最近一对括号确定。 (1) 全局变量:    全局变量是在所有函数体的外部定义的,程序的所在部分(甚至其它文件中的代码)都可以使用。全局变量不受作用域的影响...

vue 阿里云上传组件

vue 阿里云上传组件 Vue.js上传图片到阿里云OSS存储测试项目git地址 本测试项目启动方法 示例链接 组件配置项 实践解释 本文主要介绍如何 在vue项目中使用web 直传方式上传阿里云oss图片 默认读者对vue框架和阿里云oss有一定的了解整体的流程是加载好阿里云sdk -> 初始化上传客户端client -> 等待文件选择...

计算机常用端口一览表

1 传输控制协议端口服务多路开关选择器 2 compressnet 管理实用程序 3 压缩进程 5 远程作业登录 7 回显(Echo) 9 丢弃 11 在线用户 13 时间 15 netstat 17 每日引用 18 消息发送协议 19 字符发生器 20 文件传输协议(默认数据口) 21 文件传输协议(控制) 22 SSH远程登录协议 23 telnet ...

RabbitMQ重试机制

消费端在处理消息过程中可能会报错,此时该如何重新处理消息呢?解决方案有以下两种。 在redis或者数据库中记录重试次数,达到最大重试次数以后消息进入死信队列或者其他队列,再单独针对这些消息进行处理; 使用spring-rabbit中自带的retry功能; 第一种方案我们就不再详细说了,我们主要来看一下第二种方案,老规矩,先上代码: spring:...

微软Skype实时口译增加中文

直击现场   在机器翻译技术上,微软的 Skype 业务也算是行业内名列前茅。日前其实时口语翻译技术再次跃升一个台阶,新增了对中文(普通话)的翻译支持。   据美国科技新闻网站 TheVerge 报道,此前,Skype 已经实现了不同语种的人们在语音聊天时,提供实时的口语翻译。Skype 初期支持了英语和西班牙语。而在最新的语言更新中,新增了中文普通话和...

java连接redis中的数据查、增、改、删操作的方法

package com.lml.redis;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Map.Entry;import java.util.Set;import redis.clients.jedis.Jedis;publ...