Spring Cloud 異常—BeanCreationNotAllowedException: Error creating bean with name 'eurekaAutoServiceReg
Spring Clound 銷燬時報異常
BeanCreationNotAllowedException: Error creating bean with name 'eurekaAutoServiceReg
rg.springframework.beans.factory.BeanCreationNotAllowedException: Error creating bean with name 'eurekaAutoServiceRegistration': Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:216) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1081)
……
問題復現
問題復現很簡單。對於 spring-cloud 的 Dalston 版本,引入如下依賴:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> </dependencies>
編寫一個 Application,執行,在shutdown時出現錯誤。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.stereotype.Service; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class DestoryExApp { @FeignClient("test") public static interface TestClient { } @Service public static class TestSerivce { @Autowired protected TestClient testClient; } public static void main(String[] args) throws Exception { ConfigurableApplicationContext context = SpringApplication.run(DestoryExApp.class, args); Thread.sleep(2000); context.close(); } }
原因分析
銷燬順序問題。
根本原因是關閉ApplicationContext時,Spring將銷燬所有單例 bean,首先銷燬eurekaAutoServiceRegistion,然後銷燬feignContext。當銷燬 feignContext 時,同時銷燬所有與之關聯的 FeignClient。由於 eurekaAutoServiceRegistration 監聽ContextClosedEvent,這些事件將被髮送到該bean。不幸的是,因為它已經被銷燬了,所以出現上面的異常(嘗試在銷燬中建立bean)。
解決方案
作為權宜之計,調整一下銷燬順序。通過 BeanFactoryPostProcessor 改變一下依賴關係,進而影響銷燬順序。
import java.util.Arrays;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public class FeignBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (containsBeanDefinition(beanFactory, "feignContext", "eurekaAutoServiceRegistration")) {
/* 調整依賴順序,這樣會先銷燬 feignContext, 再銷燬 eurekaAutoServiceRegistration */
BeanDefinition bd = beanFactory.getBeanDefinition("feignContext");
bd.setDependsOn("eurekaAutoServiceRegistration");
}
}
private boolean containsBeanDefinition(ConfigurableListableBeanFactory beanFactory, String... beans) {
return Arrays.stream(beans).allMatch(b -> beanFactory.containsBeanDefinition(b));
}
}
詳細原因分析
當關閉ApplicationContext 時, Spring 會銷燬所有的 disposable beans (以及依賴於它們的bean)。In this case:
FeignContext 實現了 DisposableBean 介面; InetUtils 實現了 AutoCloseable 介面; EurekaServiceRegistry 擁有 public 屬性的 close 方法; 所以它們都被看作 disposable beans。
由於 EurekaAutoServiceRegistration 依賴於 InetUtils 和 EurekaServiceRegistry beans,所以他們中的的, 任何一個被銷燬,EurekaAutoServiceRegistration 也將被銷燬。 銷燬遵循先入後出的循序(FILO)。通常應用程式的程式碼不會依賴 InetUtils or EurekaServiceRegistry, 而是依賴於 FeignClient 介面.。這意味著 FeignContext 通常會先於 InetUtils 和 EurekaServiceRegistry 被建立, 而後於它們被銷燬。銷燬順序如下:
InetUtils 或 EurekaServiceRegistry 被銷燬;首先銷燬 EurekaAutoServiceRegistration ; 接著銷燬 InetUtils and EurekaServiceRegistry. 接著,銷燬 FeignContext ,將會關閉上下文中所有關聯於 FeignClients 的bean。 EurekaAutoServiceRegistration 監聽了 ContextClosedEvent 事件,但是它已經被銷燬, ApplicationContext 會嘗試再次建立它,從而獲得異常。
權宜之計:確保 InetUtils 和 EurekaServiceRegistry 先於 FeignContext 被建立。銷燬順序變更為:
銷燬 FeignContext 以及關聯的所有 FeignClients例項; EurekaAutoServiceRegistration 監聽到 ContextClosedEvent 並處理這些時間. 準備銷燬 InetUtils or EurekaServiceRegistry ,首先銷燬 EurekaAutoServiceRegistration . 接著銷燬 InetUtils 和 EurekaServiceRegistry。
@Component
public class FeignBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition bd = beanFactory.getBeanDefinition("feignContext");
bd.setDependsOn("eurekaServiceRegistry", "inetUtils");
}
}