(精)详解:Spring容器启动过程中一些内部方法的加载顺序及常见异常解决方案【@PostConstruct】【onApplicationEvent】【afterPropertiesSet】

最近一次更新时间:2020-09-19 15:53

前言

开发过程中,经常会遇到这样一种场景:希望在Spring程序启动的时候,自动做一些事情,比如初始化某些内置数据,缓存等等,这就涉及一些常用的方法,和它们加载顺序研究。本篇主要参考了另一篇博文,融合自己本地测试,整理编辑了此篇文档。

一、照例,先贴结论以及参考资料

  1. 同一个类,执行顺序固定,@PostConstruct注解的方法--->InitializingBean接口的afterPropertiesSet方法--->ApplicationListener<ContextRefreshedEvent>的onApplicationEvent方法。如果没有用到feign等依赖,就是这个顺序。
  2. 不同的类,有如下两个特点:
  • (1)@PostConstruct--->afterPropertiesSet方法,默认初始化先后顺序与类名有关,从A到Z、0到9依次执行,不能通过@Order注解来影响触发顺序
  • (2)onApplicationEvent,可以通过@Order注解去人为控制先后触发顺序。如果不控制,默认初始化先后顺序也与类名有关,从A到Z、0到9依次执行
// 有order注解的测试结果
A类的@PostConstruct注解的方法触发了
A类的afterPropertiesSet方法触发了
C类的@PostConstruct注解的方法触发了
C类的afterPropertiesSet方法触发了

T1类的@PostConstruct注解的方法触发了
T1类的afterPropertiesSet方法触发了
T2类的@PostConstruct注解的方法触发了
T2类的afterPropertiesSet方法触发了
T3类的@PostConstruct注解的方法触发了
T3类的afterPropertiesSet方法触发了

T3类的onApplicationEvent方法触发了(Order=-3)
T2类的onApplicationEvent方法触发了(Order=-2)
T1类的onApplicationEvent方法触发了(Order=-1)
C类的onApplicationEvent方法触发了(Order=1)
A类的onApplicationEvent方法触发了(Order=2)

// 无order注解 或者 order注解先后级相同时的测试结果
A类的@PostConstruct注解的方法触发了
A类的afterPropertiesSet方法触发了
C类的@PostConstruct注解的方法触发了
C类的afterPropertiesSet方法触发了

T1类的@PostConstruct注解的方法触发了
T1类的afterPropertiesSet方法触发了
T2类的@PostConstruct注解的方法触发了
T2类的afterPropertiesSet方法触发了
T3类的@PostConstruct注解的方法触发了
T3类的afterPropertiesSet方法触发了

A类的onApplicationEvent方法触发了
C类的onApplicationEvent方法触发了
T1类的onApplicationEvent方法触发了
T2类的onApplicationEvent方法触发了
T3类的onApplicationEvent方法触发了

测试类示例

@Configuration
public class T0 implements InitializingBean, ApplicationListener<ContextRefreshedEvent> {
    @PostConstruct
    public void PostConstruct(){
        System.out.println("T0类的@PostConstruct注解的方法触发了");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("T0类的afterPropertiesSet方法触发了");
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        System.out.println("T0类的onApplicationEvent方法触发了(Order=-1)");
    }
}

参考资料

1.Java spring项目启动时执行指定方法的几种方式
https://blog.csdn.net/qq_41665121/article/details/103504971
2.Spring Cloud Feign 使用 ApplicationListener 问题(精)
https://blog.csdn.net/masteryourself/article/details/106744581

二、ApplicationListener + Feign 使用常见坑点集合及原因剖析(前方排雷区,请带好安全帽)

1. ApplicationListener 中onApplication方法初始化多次

分为两种环境情况

第一种:SpringBoot(SpringCloud)工程项目

复现代码

1.BaiduFeignClient

@FeignClient(value = "baidu",url = "http://wwww.baidu.com")
public interface BaiduFeignClient {
    @GetMapping("/")
    String index();
}

2.CsdnFeignClient

@FeignClient(value = "csdn",url = "https://blog.csdn.net/")
public interface CsdnFeignClient {
    @GetMapping("/")
    String index();
}

3.MyApplicationListener

@Component
public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

    private final AtomicInteger count = new AtomicInteger(0);

    /***********************************    场景一   ***********************************/
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 初始化操作,只能做一次,但实际它会被调用多次
        System.out.println("做了一件非常重要的事情,且只能初始化一次");
    }

    /***********************************    场景二   ***********************************/
    /*@Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        String displayName = event.getApplicationContext().getDisplayName();
        // 第[1]次调用,context 上下文是:FeignContext-baidu
        // 第[2]次调用,context 上下文是:FeignContext-csdn
        // 第[3]次调用,context 上下文是:org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7d3e8655
        System.out.println("第[" + count.incrementAndGet() + "]次调用,context 上下文是:" + displayName);
        // 仅仅适用于 spring cloud F 版本之后,F 版本之前可使用 AtomicBoolean 来判断(因为没有设置 displayName)
        if (displayName.startsWith(FeignContext.class.getSimpleName())) {
            return;
        }
        // 初始化操作,只能做一次
        System.out.println("做了一件非常重要的事情,且只能初始化一次");
    }*/
}

异常剖析

(1) ApplicationListener 回调机制

在 Spring 容器在创建过程中,都会调用 refresh() 刷新方法,在这个方法的最后一步即是 finishRefresh(),然后用它来发布 ContextRefreshedEvent 事件,它会从容器中找出所有的 ApplicationListener,然后循环调用它们的 onApplicationEvent() 方法

(2) Feign 原理

@EnableFeignClients -> FeignClientsRegistrar -> 扫描 FeignClient 注解,设置 BeanDefinition 的 BeanClass 类型为 FeignClientFactoryBean,它是 FactoryBean 类型,通过 getObject() 方法获取 Feign 实例

在调用 getObject() 方法获取对象时,底层会调用 NamedContextFactorycreateContext() 方法创建一个单独的 FeignContext 上下文对象,目的就是为了配置隔离,所以最终每一个 FeignContext 都会调用 refresh() 方法进行刷新操作,这也就造成了我们定义的 ApplicationListener 中的 onApplicationEvent()方法被调用了多次

解决办法也很简单,Spring 在创建每个 Feign 组件时,会调用 context.setDisplayName(generateDisplayName(name)) 方法设置 displayNamegenerateDisplayName() 的生成规则就是 FeignContext-xxx(xxx 是 @FeignClient 注解中的 value 属性),所以使用注释中的场景二即可解决。

但要注意:这里只适用于 Spring Cloud F 版本之后,在这之前,Spring Cloud Feign 组件并没有调用 setDisplayName() 这个方法赋值,所以可以使用 AtomicBoolean 来判断

第二种:SpringMVC web工程

在spring mvc项目中,系统会存在两个容器,一个是root application context,另一个就是我们自己的 projectName-servlet context(作为root application context的子容器)。
启动时,父容器与子容器先后被加载,于是就会造成onApplicationEvent方法被执行两次。为了避免上面提到的问题,可以有两种做法:

  1. 我们可以在类中自己定义一个静态变量,用于标志是否已执行过,比如hasInited等
  2. 我们也可以只在root application context初始化完成后调用逻辑代码,其他的容器的初始化完成,则不做任何处理,则改动代码如下
public class startBeanPostProcessor implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { 
        if (contextRefreshedEvent.getApplicationContext().getParent() == null) {//root application context 没有parent,他就是老大.
             //需要执行的逻辑代码,当spring容器初始化完成后就会执行该方法。
        }
    }
}

2.ApplicationListener 中使用调用某个类方法报 NPE(NullPointException异常)

复现代码

/**
 * <p>description : MyApplicationListener, 监听容器刷新事件
 * 1. 如果先注入了 {@link BaiduFeignClient}, 再注入 {@link SomeBean}, spring 调用 onApplicationEvent() 方法的过程如下(第一次 someBean 无值):
 * {@link FeignListenerNpeApplication} -> refresh(4) -> baiduFeignClient -> refresh(2) -> client -> refresh(1) + {@link SomeBean}
 *                                                   -> csdnFeignClient -> refresh(3)
 *
 * 2. 如果先注入了 {@link SomeBean}, 再注入 {@link BaiduFeignClient}, spring 调用 onApplicationEvent() 方法的过程如下(第一次 someBean 有值):
 * {@link FeignListenerNpeApplication} -> refresh(4) -> baiduFeignClient -> refresh(2) -> {@link SomeBean} + client -> refresh(1)
 *                                                   -> csdnFeignClient -> refresh(3)
 *
 * <p>blog : https://blog.csdn.net/masteryourself
 *
 * @author : masteryourself
 * @version : 1.0.0
 * @date : 2020/6/9 10:56
 */
@Component
public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

    /***********************************    场景一   ***********************************/
    @Autowired
    private BaiduFeignClient client;

    @Autowired
    private SomeBean someBean;

    /***********************************    场景二   ***********************************/
    /*@Autowired
    private SomeBean someBean;

    @Autowired
    private BaiduFeignClient client;*/

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("context 上下文是:" + event.getApplicationContext().getDisplayName());
        someBean.doSomething();
    }

}

异常剖析

(1)Spring Cloud Feign 创建时机

场景一代码:先为 client 对象赋值,而它是一个 Feign 对象,所以在初始化 Feign 对象时,将会执行 refersh() 方法刷新,而在刷新过程中,将会触发 onApplicationEvent() 事件,最终导致在方法里使用的 someBean 对象是空的,此时的执行流程图为:

image

场景二代码:先为 someBean 对象赋值,然后再为 client 对象赋值,所以在 onApplicationEvent() 方法里不会抛出 NPE 异常,此时的执行流程图为:

image

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355