Нужно ли уничтожать компоненты прототипа Spring вручную?

Я заметил, что перехватчики @PreDestroy моих компонентов Spring с областью видимости прототипа не выполнялись.

С тех пор я прочитал здесь, что это на самом деле задумано. Контейнер Spring уничтожит одноэлементные bean-компоненты, но не уничтожит их прототипы. Мне непонятно почему. Если контейнер Spring создаст мой прототип bean-компонента и выполнит свой хук @PostConstruct, почему он не уничтожит и мой bean-компонент, когда контейнер закрыт? Когда мой контейнер Spring был закрыт, имеет ли смысл продолжать использовать какой-либо из его bean-компонентов? Я не вижу сценария, в котором вы хотели бы закрыть контейнер до того, как закончите с его bean-компонентами. Можно ли продолжать использовать прототип компонента Spring после закрытия его контейнера?

Вышеупомянутое описывает загадочную подоплеку моего основного вопроса: если контейнер Spring не уничтожает прототипы bean-компонентов, означает ли это, что может произойти утечка памяти? Или в какой-то момент компонент-прототип получит сборщик мусора?

В документации Spring говорится:

The client code must clean up prototype-scoped objects and release expensive resources that the prototype bean(s) are holding. To get the Spring container to release resources held by prototype-scoped beans, try using a custom bean post-processor, which holds a reference to beans that need to be cleaned up.

Что это обозначает? Текст подсказывает мне, что я, как программист, несу ответственность за явное (вручную) уничтожение моих прототипов beans. Это правильно? Если да, то как мне это сделать?

Вам не нужно ничего делать, пока вам не нужно освобождать ресурсы, используемые вашим прототипом bean-компонента. например если вы создаете пул соединений с БД в своем прототипе bean-компонента с областью видимости, вам, вероятно, нужно закрыть его :)

Michal 04.06.2018 15:03

Spring не знает, каков жизненный цикл компонента-прототипа, поэтому он будет вызывать только инициализаторы, а не обратные вызовы уничтожения, поскольку он просто не работает (и не может) сейчас, когда он вам больше не нужен.

M. Deinum 04.06.2018 15:05

Спасибо вам обоим. @ M.Deinum, то, что вы сказали, я обсуждал в своем вопросе. Закрытие контейнера Spring должно дать Spring большой намек на то, что мы закончили с нашими прототипами beans. После закрытия контейнера можно ли продолжать использовать любой компонент Spring (даже компонент-прототип)? Даже если это так, цитата из документации Spring предполагает, что, если я не уничтожу bean-компонент, произойдет утечка памяти. Вот что меня пугает. Я слышал Михала, который предполагает, что сборщик мусора подберет его, как только объектная переменная в компоненте-прототипе выйдет за пределы области видимости.

IqbalHamid 04.06.2018 15:31

Нет, это не так. Как обстоят дела с Spring теперь, когда все еще существуют прототипы beans? Некоторые используют контекст с прототипами bean-компонентов как фабрику для короткоживущих bean-компонентов (или для получения чертежа bean-компонента). Теоретически могли быть созданы тысячи экземпляров прототипов bean-компонентов, которые уже были обработаны сборщиком мусора. Spring этого просто не знает. Утечка памяти возникает только в том случае, если этот прототип beans содержит ссылку на что-то, что предотвращает сборку мусора.

M. Deinum 04.06.2018 15:34

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

IqbalHamid 04.06.2018 15:37

Спасибо @ M.Deinum, ваш комментарий имеет смысл.

IqbalHamid 04.06.2018 15:39
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
26
6
12 996
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Для пользы других я представлю ниже то, что я собрал в результате своих исследований:

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

Однако в случае, когда может произойти утечка памяти, как описано выше, компоненты-прототипы могут быть уничтожены путем создания постпроцессора одноэлементного компонента, метод уничтожения которого явно вызывает перехватчики уничтожения ваших компонентов-прототипов. Поскольку постпроцессор сам является одноэлементным, его ловушка уничтожения будут вызывается Spring:

  1. Создайте постпроцессор bean для обработки уничтожения всех ваших прототипов bean-компонентов. Это необходимо, потому что Spring не уничтожает прототипы bean-компонентов, и поэтому любые хуки @PreDestroy в вашем коде никогда не будут вызваны контейнером.

  2. Реализуйте следующие интерфейсы:

    1.BeanFactoryAware
    Этот интерфейс предоставляет метод обратного вызова, который получает объект Beanfactory. Этот объект BeanFactory используется в классе постпроцессора для идентификации всех компонентов-прототипов с помощью его метода BeanFactory.isPrototype (String beanName).

    2. Одноразовые
    Этот интерфейс предоставляет метод обратного вызова Destroy (), вызываемый контейнером Spring. Мы будем вызывать методы Destroy () всех наших прототипов bean-компонентов из этого метода.

    3. BeanPostProcessor
    Реализация этого интерфейса обеспечивает доступ к обратным вызовам пост-обработки, из которых мы готовим внутренний List <> всех объектов-прототипов, созданных контейнером Spring. Позже мы переберем этот List <>, чтобы уничтожить каждый из наших прототипов bean-компонентов.


3. Наконец, реализуйте интерфейс DisposableBean в каждом из ваших прототипов bean-компонентов, предоставив метод Destroy (), требуемый этим контрактом.

Чтобы проиллюстрировать эту логику, я привожу ниже код, взятый из этого статья:

/**
* Bean PostProcessor that handles destruction of prototype beans
*/
@Component
public class DestroyPrototypeBeansPostProcessor implements BeanPostProcessor, BeanFactoryAware, DisposableBean {

    private BeanFactory beanFactory;

    private final List<Object> prototypeBeans = new LinkedList<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanFactory.isPrototype(beanName)) {
            synchronized (prototypeBeans) {
                prototypeBeans.add(bean);
            }
        }
        return bean;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public void destroy() throws Exception {
        synchronized (prototypeBeans) {
            for (Object bean : prototypeBeans) {
                if (bean instanceof DisposableBean) {
                    DisposableBean disposable = (DisposableBean)bean;
                    try {
                        disposable.destroy();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            prototypeBeans.clear();
        }
    }
}

Собирает ли компонент-прототип сборщик мусора, если он не содержит ссылки на сеанс или пул, но содержит ссылки на другие службы (компоненты без состояния). Бин-прототип вызывает метод транзакционных служб. Таким образом, bean-компонент сам по себе не является транзакцией и не имеет сеанса, а внедренный beand-компонент.

Akshay 26.10.2018 15:22

По какой-то причине beanFactory.isPrototype() ломается в интеграционных тестах с NoSuchBeanDefinitionException на самом интеграционном тестовом компоненте. В остальном этот подход потрясающий.

sjngm 11.11.2019 10:29

В качестве дополнительной заметки лучше использовать Deque<Object> prototypeBeans = new ArrayDeque<>(); и push() вместо add(), чтобы цикл выполнялся в обратном порядке. Никогда не знаешь, есть ли дополнительные зависимости между bean-компонентами ...

sjngm 11.11.2019 12:22

И снова интеграционные тесты: не забудьте добавить @DirtiesContext в интеграционный тест, если он создает экземпляр bean-компонента с областью видимости прототипа.

sjngm 17.12.2019 14:17

Если мы используем приложение весенней загрузки на основе отдыха, в какой ситуации будет вызван метод уничтожения DestroyPrototypeBeansPostProcessor?

Suneet Khurana 13.09.2020 13:56

Я понимаю, как это происходит, но я не понимаю, почему. вы говорите, поскольку есть случай, когда у прототипов beans есть db, ссылки на сеанс, бремя должно ложиться на клиента, а не на структуру, чтобы уничтожить его с помощью специального процессора. Но почему?

susheelbhargavk 01.12.2020 23:02

"Пока компонент-прототип не содержит ссылку на другой ресурс, такой как соединение с базой данных или объект сеанса, он будет собирать мусор, как только все ссылки на объект будут удалены или объект выйдет за пределы области видимости. " Как вы думаете, почему bean-компонент не будет собираться, если он поддерживает соединение с базой данных?

Dragos Ionut 16.02.2021 19:12

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

Недавно я написал отвечать в отдельный вопрос о внутренних bean-компонентах. Внутренние bean-компоненты создаются путем присвоения значений свойств bean-компонентам как объекты BeanDefinition. Значения свойств определения компонента автоматически разрешаются в экземпляры (inner) (как управляемые одноэлементные компоненты) того компонента, который они определяют.

Следующий элемент конфигурации контекста XML может использоваться для создания отдельных автоматически подключаемых bean-компонентов ForkJoinPool для каждой управляемой ссылки (@PreDestroy будет вызываться при выключении контекста):

<!-- Prototype-scoped bean for creating distinct FJPs within the application -->
<bean id = "forkJoinPool" class = "org.springframework.beans.factory.support.GenericBeanDefinition" scope = "prototype">
    <property name = "beanClass" value = "org.springframework.scheduling.concurrent.ForkJoinPoolFactoryBean" />
</bean>

Однако такое поведение зависит от того, назначена ли ссылка как стоимость имущества определения компонента. Это означает, что @Autowired и внедрение конструктора не работают с этим по умолчанию, поскольку эти методы автоматического подключения разрешают значение немедленно, а не используют разрешение значения свойства в AbstractAutowireCapableBeanFactory#applyPropertyValues. Автоматическое связывание по типу также не будет работать, поскольку разрешение типа не будет распространяться через bean-компоненты, являющиеся BeanDefinition, для поиска произведенного типа.

Этот метод будет работать, только если выполняется одно из двух условий:

  • Зависимые bean-компоненты - это также, определенные в XML.
  • Или же, если режим autowire установлен на AutowireCapableBeanFactory#AUTOWIRE_BY_NAME

<!-- Setting bean references through XML -->
<beans ...>
    <bean id = "myOtherBean" class = "com.example.demo.ForkJoinPoolContainer">
        <property name = "forkJoinPool" ref = "forkJoinPool" />
    </bean>
</beans>

<!-- Or setting the default autowire mode -->
<beans default-autowire = "byName" ...>
    ...
</beans>

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

  • Конструктор-инъекция:

    Фабрика компонентов назначает AutowireCandidateResolver для внедрения конструктора. Значение по умолчанию (ContextAnnotationAutowireCandidateResolver) можно переопределить (DefaultListableBeanFactory#setAutowireCandidateResolver), чтобы применить распознаватель кандидатов, который ищет подходящие компоненты типа BeanDefinition для внедрения.

  • @Autowired-инъекция:

    Постпроцессор bean-компонентов AutowiredAnnotationBeanPostProcessor напрямую устанавливает значения bean-компонентов без разрешения внутренних компонентов BeanDefinition. Этот постпроцессор можно переопределить, или можно создать отдельный постпроцессор bean для обработки пользовательской аннотации для управляемых компонентов-прототипов (например, @AutowiredManagedPrototype).

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