1. 场景前提
假设有这样一个场景:在一个 Spring Cloud Feign(Greenwich.SR6)应用中,希望在 Spring 容器启动之后对一些事件做监听,如接收到 ContextRefreshedEvent
事件后,需要做一次初始化操作。一般都是实现 ApplicationListener
接口来监听事件,然后在 onApplicationEvent()
方法里做相应的处理
此时可能会遇到两种情况,一个是监听器中的 onApplicationEvent()
方法被调用了多次,还有一个即是在监听器中使用一些 bean 可能会抛出 NPE 异常
2. ApplicationListener 中初始化多次
2.1 环境搭建
代码已经上传至 https://github.com/masteryourself/diseases ,详见
diseases-spring-cloud/diseases-spring-cloud-feign-listener
工程
2.1.1 代码
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("做了一件非常重要的事情,且只能初始化一次");
}*/
}
2.2 异常剖析
2.2.1 ApplicationListener 回调机制
在 Spring 容器在创建过程中,都会调用 refresh()
刷新方法,在这个方法的最后一步即是 finishRefresh()
,然后用它来发布 ContextRefreshedEvent
事件,它会从容器中找出所有的 ApplicationListener
,然后循环调用它们的 onApplicationEvent()
方法
2.2.2 Spring Cloud Feign 原理
@EnableFeignClients
-> FeignClientsRegistrar
-> 扫描 FeignClient
注解,设置 BeanDefinition
的 BeanClass
类型为 FeignClientFactoryBean
,它是 FactoryBean
类型,通过 getObject()
方法获取 Feign 实例
在调用 getObject()
方法获取对象时,底层会调用 NamedContextFactory#createContext()
方法创建一个单独的 Spring Context 上下文对象,目的就是为了配置隔离,所以最终每一个 Spring Context 都会调用 refresh()
方法进行刷新操作,这也就造成了我们定义的 ApplicationListener
中的 onApplicationEvent()
方法被调用了多次
解决办法也很简单,Spring 在创建每个 Feign 组件时,会调用 context.setDisplayName(generateDisplayName(name))
方法设置 displayName,generateDisplayName()
的生成规则就是 FeignContext-xxx
(xxx 是 @FeignClient
注解中的 value 属性),所以使用场景二即可解决。
但要注意:这里只适用于 Spring Cloud F 版本之后,在这之前,Spring Cloud Feign 组件并没有调用 setDisplayName()
这个方法赋值,所以可以使用 AtomicBoolean
来判断
3. ApplicationListener 中使用组件导致 NPE
3.1 环境搭建
代码已经上传至 https://github.com/masteryourself/diseases ,详见
diseases-spring-cloud/diseases-spring-cloud-feign-listener-npe
工程
3.1.1 代码
1. MyApplicationListener
/**
* <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();
}
}
3.2 异常剖析
3.2.1 Spring Cloud Feign 创建时机
场景一代码:先为 client
对象赋值,而它是一个 Feign 对象,所以在初始化 Feign 对象时,将会执行 refersh()
方法刷新,而在刷新过程中,将会触发 onApplicationEvent()
事件,最终导致在方法里使用的 someBean
对象是空的,此时的执行流程图为:
场景二代码:先为 someBean
对象赋值,然后再为 client
对象赋值,所以在 onApplicationEvent()
方法里不会抛出 NPE 异常,此时的执行流程图为: