Порядок уничтожения бинов в контекстной иерархии Spring

Правильно ли говорить, что когда иерархия контекста Spring закрыта, не существует гарантированного порядка, в котором компоненты будут уничтожены? Например, бины в дочернем контексте будут уничтожены до родительского контекста. Из минимального примера разрушение контекстов кажется совершенно не скоординированным между контекстами (как ни странно). В обоих контекстах регистрируется хук отключения, который в дальнейшем будет выполняться в разных потоках.

@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);
    }

}
}

Дает вывод:

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

Есть ли способ заставить контексты быть закрыты в "правильном" порядке?

1 ответ

Решение

Я только что выкопал в той же самой проблеме сам, и все больше не выглядит странным. Хотя я все еще хотел, чтобы все было иначе.

Когда у вас есть родительский и дочерний контексты Spring, родитель ничего не знает о дочернем элементе. Вот как спроектирован Spring, и это верно для всех настроек.

Теперь могут быть некоторые различия

Веб-приложение в контейнере сервлетов

Наиболее распространенной настройкой для этого случая (не считая настройки одного контекста) является объявление корневого контекста с помощью ContextLoaderListener и дочерний контекст с помощью DispatcherServlet,

Когда веб-приложение (или контейнер) закрывается как ContextLoaderListener а также DispatcherServlet получать уведомления через ServletContextListener.contextDestroyed(...) а также Servlet.destroy() соответственно.

Согласно Javadoc, сначала сервлеты и фильтры уничтожаются и только после того, как они сделаны ServletContextListenerэто.

Таким образом, в веб-приложениях, запущенных в контейнере сервлетов DispatcherServlet контекст (который является дочерним) сначала уничтожается, и только затем корневой контекст уничтожается.

Автономное веб-приложение

Следующее в равной степени верно не только для автономных приложений, но и для любых автономных приложений Spring, использующих иерархический контекст.

Поскольку контейнера нет, приложению stanadlone необходимо связаться с самой JVM, чтобы получить сигнал отключения и обработать его. Это делается с помощью механизма отключения крючков.

Spring не пытается определить среду, в которой он работает, за исключением возможностей \ версии JVM (но Spring Boot может отлично справиться с автоматическим определением env).

Поэтому, чтобы Spring зарегистрировал перехватчик завершения работы, вы должны сказать это, когда создаете контекст ( javadoc). Если вы этого не сделаете, у вас не будет @PreDestroy/DisposableBean обратные вызовы, вызываемые на всех.

Как только вы зарегистрируете ловушку отключения контекста в JVM, он будет уведомлен и будет правильно обрабатывать завершение работы для этого контекста.

Если у вас есть родительско-дочерние контексты, вы можете захотеть .registerShutdownHook() для каждого из них. Это будет работать в некоторых случаях. Но JVM вызывает перехватчики отключения в недетерминированном порядке, так что, к сожалению, это не решает проблему.

Теперь, что мы могли об этом

Вероятно, самым простым (хотя и не самым элегантным) решением было бы иметь ApplicationListener<ContextClosedEvent> или же DisposableBean сидя в родительском контексте и

  • имея ссылку [s] на дочерний контекст [s]
  • хранение bean-компонентов из родительского контекста, которые являются критическими для дочернего контекста (например, db-соединения), так что они сохраняются до тех пор, пока дочерний контекст не активен @Autowire или же @DependsOn или с xml аналогами)
  • закрытие дочернего контекста [s] до закрытия родительского контекста

JUnit тесты

Оригинальный вопрос представлен тестом JUnit. Я на самом деле не так много копал, но подозреваю, что с этим снова все по-другому. Так как это SpringJUnit4ClassRunner который управляет ими всеми и прежде всего контекстными иерархиями. С другой стороны, тесты JUnit имеют четко определенный и управляемый жизненный цикл (в значительной степени контейнер сервлетов со списком).

В любом случае, поняв внутреннюю работу, я полагаю, вам будет легко решить эту проблему:)

Другие вопросы по тегам