最近一次更新时间:2020-09-19 15:53
前言
开发过程中,经常会遇到这样一种场景:希望在Spring程序启动的时候,自动做一些事情,比如初始化某些内置数据,缓存等等,这就涉及一些常用的方法,和它们加载顺序研究。本篇主要参考了另一篇博文,融合自己本地测试,整理编辑了此篇文档。
一、照例,先贴结论以及参考资料
-
同一个类,执行顺序固定,
@PostConstruct
注解的方法--->InitializingBean接口的afterPropertiesSet
方法--->ApplicationListener<ContextRefreshedEvent>的onApplicationEvent
方法。如果没有用到feign等依赖,就是这个顺序。 - 不同的类,有如下两个特点:
- (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() 方法获取对象时,底层会调用 NamedContextFactory
的createContext()
方法创建一个单独的 FeignContext 上下文对象,目的就是为了配置隔离,所以最终每一个 FeignContext 都会调用 refresh() 方法进行刷新操作,这也就造成了我们定义的 ApplicationListener 中的 onApplicationEvent()
方法被调用了多次
解决办法也很简单,Spring 在创建每个 Feign 组件时,会调用 context.setDisplayName(generateDisplayName(name))
方法设置 displayName
,generateDisplayName()
的生成规则就是 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方法被执行两次。为了避免上面提到的问题,可以有两种做法:
- 我们可以在类中自己定义一个静态变量,用于标志是否已执行过,比如hasInited等
- 我们也可以只在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
对象是空的,此时的执行流程图为:
场景二代码:先为 someBean
对象赋值,然后再为 client 对象赋值,所以在 onApplicationEvent() 方法里不会抛出 NPE 异常,此时的执行流程图为:
网友评论