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()
方法創建一個單獨的 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
來判斷
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 異常,此時的執行流程圖爲: