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 注解,设置 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 异常,此时的执行流程图为:

场景二

==========

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

异常剖析

(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容器初始化完成后就会执行该方法。
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值