Spring Clound 销毁时报异常
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)……
原因分析及解决方案参考:https://github.com/spring-cloud/spring-cloud-netflix/issues/1952 。
问题复现很简单。对于 spring-cloud 的 Dalston 版本,引入如下依赖:
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.cloud
spring-cloud-starter-feign
编写一个 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");
}
}