003.Spring Cloud Feign 使用 ApplicationListener 問題

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 註解,設置 BeanDefinitionBeanClass 類型爲 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 異常,此時的執行流程圖爲:

場景二

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章