1. Scenario premise
Suppose there is a scenario: in a Spring Cloud Feign( Greenwich.SR6 )In the application, you want to listen to some events after the Spring container is started. For example, you need to do an initialization operation after receiving the ContextRefreshedEvent event. Generally, it is to implement the ApplicationListener interface to listen for events, and then do the corresponding processing in the onApplicationEvent() method
At this point, two situations may be encountered: one is that the onApplicationEvent() method in the listener has been called multiple times, and the other is that using some bean s in the listener may throw NPE exceptions
2. Initialization multiple times in applicationlistener
2.1 environment construction
Code has been uploaded to https://github.com/masteryourself/diseases For details, please refer to the releases spring cloud / releases spring cloud feign listener project
2.1.1 code
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); /*********************************** Scenario 1***********************************/ @Override public void onApplicationEvent(ContextRefreshedEvent event) { // Initialization operation can only be done once, but it will be called multiple times System.out.println("Did a very important thing, and can only initialize once"); } /*********************************** Scenario 2***********************************/ /*@Override public void onApplicationEvent(ContextRefreshedEvent event) { String displayName = event.getApplicationContext().getDisplayName(); // The [1] call, the context context is: feigncontext Baidu // The [2] call, the context context is: feigncontext CSDN // The [3] call, the context context is: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@7d3e8655 System.out.println("The ["+ count.incrementAndGet() + "] calls, context context is:" + displayName); // Only after spring cloud version F, AtomicBoolean can be used to judge before version f (because displayName is not set) if (displayName.startsWith(FeignContext.class.getSimpleName())) { return; } // Initialization operation, can only be done once System.out.println("Did a very important thing, and can only be initialized once "); }*/ }
2.2 abnormal analysis
2.2.1 ApplicationListener callback mechanism
During the creation of Spring containers, the refresh() refresh method will be called. The last step of this method is to finish refresh(), and then use it to publish ContextRefreshedEvent events. It will find all applicationlisteners from the containers, and then cycle their onApplicationEvent() methods
2.2.2 principle of spring cloud feign
@Enable feignclients - > feignclientsregister - > scan FeignClient annotation, set BeanClass type of BeanDefinition to FeignClientFactoryBean, which is FactoryBean type, and get Feign instance through getObject() method
When the getObject() method is called to get the object, the underlying layer will call the namedcontextfactory ᦇ createcontext() method to create a separate FeignContext object, the purpose of which is to configure isolation, so eventually each FeignContext will call the refresh() method to refresh, which also causes the onApplicationEvent() method called more than once
The solution is also simple. When Spring creates each Feign component, it calls context.setDisplayName(generateDisplayName(name)) method is used to set displayName. The generation rule of generateDisplayName() is feigncontext xxx (xxx is the value attribute in the @ FeignClient annotation). So scenario 2 can be used to solve this problem.
Note: this only applies after the Spring Cloud F version. Before that, the Spring Cloud Feign component did not call the setDisplayName() method to assign a value, so you can use AtomicBoolean to judge
3. NPE caused by using components in applicationlistener
3.1 environment construction
Code has been uploaded to https://github.com/masteryourself/diseases For details, please refer to releases spring cloud / releases spring cloud feign listener NPE project
3.1.1 code
1. MyApplicationListener
/** * <p>description : MyApplicationListener, Listen for container refresh events * 1. If {@ link BaiduFeignClient} and {@ link someBean} are injected first, the procedure of spring calling onApplicationEvent() method is as follows (the first someBean has no value): * {@link FeignListenerNpeApplication} -> refresh(4) -> baiduFeignClient -> refresh(2) -> client -> refresh(1) + {@link SomeBean} * -> csdnFeignClient -> refresh(3) * * 2. If {@ link SomeBean} is injected first, and then {@ link baidufeignclient}, the procedure of spring calling onApplicationEvent() method is as follows (the first time someBean has value): * {@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> { /*********************************** Scenario 1***********************************/ @Autowired private BaiduFeignClient client; @Autowired private SomeBean someBean; /*********************************** Scenario 2***********************************/ /*@Autowired private SomeBean someBean; @Autowired private BaiduFeignClient client;*/ @Override public void onApplicationEvent(ContextRefreshedEvent event) { System.out.println("context The context is:" + event.getApplicationContext().getDisplayName()); someBean.doSomething(); } }
3.2 abnormal analysis
3.2.1 creation time of spring cloud feign
Scenario 1 Code: first, assign a value to the client object, which is a Feign object, so when the Feign object is initialized, the refresh of the refresh() method will be executed, and during the refresh process, the onApplicationEvent() event will be triggered, resulting in that the someBean object used in the method is empty. The execution flow chart at this time is:
Scenario 2 Code: assign a value to someBean object first, and then to client object, so NPE exception will not be thrown in onApplicationEvent() method. The execution flow chart at this time is: