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.youkuaiyun.com/")
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.youkuaiyun.com/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 异常,此时的执行流程图为:

340



被折叠的 条评论
为什么被折叠?



