Spring 上下文层次结构中 bean 销毁的顺序

hea*_*tar 7 java spring spring-test hierarchical context-configuration

当 Spring 上下文层次结构关闭时,没有保证 bean 被销毁的顺序,这样说是否正确?例如,子上下文中的 bean 将在父上下文之前被销毁。从一个最小的例子来看,上下文的破坏似乎在上下文之间完全不协调(奇怪的是)。两个上下文都注册了一个关闭钩子,该钩子稍后将在不同的线程中执行。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({
    @ContextConfiguration(classes = {ATest.Root.class}),
    @ContextConfiguration(classes = {ATest.Child.class})
})
public class ATest {

@Test
public void contextTest() {
}

public static class Root {
    @Bean
    Foo foo() {
        return new Foo();
    }
}


public static class Child {
    @Bean
    Bar bar() {
        return new Bar();
    }

}

static class Foo {
    Logger logger = LoggerFactory.getLogger(Foo.class);
    volatile boolean destroyed;

    @PostConstruct
    void setup() {
        logger.info("foo setup");

    }

    @PreDestroy
    void destroy() {
        destroyed = true;
        logger.info("foo destroy");
    }

}

static class Bar {

    @Autowired
    Foo foo;

    Logger logger = LoggerFactory.getLogger(Bar.class);

    @PostConstruct
    void setup() {
        logger.info("bar setup with foo = {}", foo);
    }

    @PreDestroy
    void destroy() {
        logger.info("bar destroy, foo is destroyed={}", foo.destroyed);
    }

}
}
Run Code Online (Sandbox Code Playgroud)

给出输出:

21:38:53.287 [Test worker] INFO ATest$Foo - foo setup
21:38:53.327 [Test worker] INFO ATest$Bar - bar setup with foo = com.tango.citrine.spring.ATest$Foo@2458117b
21:38:53.363 [Thread-4] INFO ATest$Foo - foo destroy
21:38:53.364 [Thread-5] INFO ATest$Bar - bar destroy, foo is destroyed=true
Run Code Online (Sandbox Code Playgroud)

有没有办法强制以“正确”的顺序关闭上下文?

sco*_*rpp 6

我自己也研究过同样的问题,一切看起来都不再奇怪了。尽管我仍然希望这种行为有所不同。

当您有父级和子级 Spring 上下文时,父级对子级一无所知。这就是 Spring 的设计方式,并且对于所有设置都是如此。

现在可能有一些区别

Servlet 容器中的 Web 应用程序

这种情况最常见的设置(不包括单上下文设置)是使用 声明根上下文并ContextLoaderListener通过 声明子上下文DispatcherServlet

当 webapp(或容器)关闭时, 和会ContextLoaderListener相应地DispatcherServlet收到通知。ServletContextListener.contextDestroyed(...)Servlet.destroy()

根据javadoc,首先 servlet 和过滤器被销毁,并且只有在它们完成后才被销毁ServletContextListener

因此,在 Servlet 容器中运行的 Web 应用程序中,DispatcherServlet首先销毁上下文(子上下文),然后才销毁根上下文。

独立网络应用程序

以下内容不仅适用于独立 weapp,也适用于任何使用分层上下文的独立 Spring 应用。

由于没有容器,因此独立应用程序需要与 JVM 本身进行通信,以便接收关闭信号并处理它。这是使用关闭挂钩机制完成的。

除了 JVM 功能\版本之外,Spring 不会尝试推断其运行的环境(但 Spring Boot 在自动推断 env 方面可以做得很好)。

因此,要让 Spring 注册一个关闭钩子,您需要在创建上下文 ( javadoc )时指定它这样做。如果您不这样做,您将根本不会调用@PreDestroy/回调。DisposableBean

一旦您向 JVM 注册了上下文的关闭钩子,它将收到通知并正确处理该上下文的关闭。

如果您有亲子关系,您可能需要.registerShutdownHook()为每个人提供。这对于某些情况有效。但 JVM 以不确定的顺序调用关闭挂钩,因此不幸的是,这并不能真正解决主题问题。

现在我们能做什么呢

可能最简单(尽管不是最优雅)的解决方案是 在父上下文中使用ApplicationListener<ContextClosedEvent>or ,并且DisposableBean

  • 引用子上下文
  • 从父上下文中保存对子上下文(例如数据库连接)至关重要的bean,以便保留它们直到子上下文处于活动状态(这可以使用@Autowire@DependsOn或 xml 对应项来完成)
  • 在关闭父上下文之前关闭子上下文

JUnit 测试

最初的问题提出了 JUnit 测试。我并没有真正挖掘那么深,但我怀疑事情又与这个不同了。因为它SpringJUnit4ClassRunner首先统治着它们和上下文层次结构。另一方面,JUnit 测试具有良好定义和管理的生命周期(几乎是列表 servlet 容器)。

不管怎样,在理解了内部工作原理后,我相信你应该很容易解决这个问题:)