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() 方法创建一个单独的 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 异常,此时的执行流程图为:

场景二

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
1. Spring Cloud是基于Spring Framework的微服务框架,它提供了一系列开箱即用的工具和组件,用于构建分布式系统中的常见模式,如服务发现、负载均衡、断路器、配置管理等。 2. Eureka是Spring Cloud中的一个服务发现组件,它允许微服务应用程序注册自己以及发现其他注册的应用程序实例。在Eureka中,服务提供者会向Eureka服务器注册自己的信息,而服务消费者则通过Eureka服务器获得可用的服务列表。 3. Ribbon是Spring Cloud中的一个负载均衡组件,它可以根据一定的负载均衡策略,将客户端的请求分发到多个服务提供者之间,从而提高系统的可用性和性能。 4. FeignSpring Cloud中的一个声明式HTTP客户端,它通过注解的方式,定义了REST API的接口,Feign会根据这些接口定义生成具体的HTTP请求代码,从而简化了微服务之间的调用。 5. Zuul是Spring Cloud中的一个API网关组件,它提供了一系列的过滤器来实现请求的路由、过滤和转发等功能,可以有效地对外部请求进行管理和控制。 6. Hystrix是Spring Cloud中的一个容错框架,它可以在微服务之间添加断路器,当某个服务出现故障时,Hystrix可以快速地切换到备用方案,从而保证整个系统的稳定性和可用性。 7. Turbine是Spring Cloud中的一个聚合监控组件,它可以将多个Hystrix Dashboard的数据聚合起来,从而方便开发人员进行统一的监控和分析。 8. Config是Spring Cloud中的一个配置管理组件,它可以将应用程序的配置信息集中管理,从而可以实现对分布式系统中各个微服务的配置进行集中管理。 9. Sleuth是Spring Cloud中的一个分布式跟踪组件,它可以用于监控和跟踪微服务之间的调用关系,从而方便开发人员进行故障排查和性能优化。 10. Bus是Spring Cloud中的一个事件总线组件,它可以用于实现微服务之间的事件传递和状态同步,从而方便开发人员进行系统的监控和管理。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值