Порядок уничтожения bean-компонентов в контекстной иерархии Spring

Правильно ли говорить, что когда иерархия контекста Spring закрыта, не существует гарантированного порядка уничтожения компонентов? Например. 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);
    }

}
}

Дает вывод:

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

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


person headstar    schedule 04.03.2016    source источник


Ответы (1)


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

JUnit-тесты

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

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

person scorpp    schedule 13.12.2016
comment
Благодаря достаточному объяснению, я смог решить свою проблему. Спасибо. - person NCrash; 11.12.2020