Для взаємодії з механізмом управління життєвим циклом бінів контейнера можна реалізувати інтерфейси InitializingBean та DisposableBean у Spring. Контейнер викликає afterPropertiesSet() для першого та destroy() для другого, щоб бін міг виконати певні дії під час ініціалізації та знищення твоїх бінів.

Анотації @PostConstruct та @PreDestroy, стандартизовані JSR-250, зазвичай вважаються кращою методикою отримання зворотних викликів життєвого циклу в сучасному додатку на Spring. Використання цих анотацій означає, що твоі біни не пов'язані з інтерфейсами, специфічними для Spring.

Якщо не бажаєш використовувати анотації JSR-250, але все ж таки хочеш усунути зв'язаність, розглянь метадані визначення біна init-method та destroy-method.

Всередині Spring Framework використовує реалізації BeanPostProcessor для обробки будь-яких інтерфейсів зворотного виклику, які він може знайти, та виклику відповідних методів. Якщо тобі потрібні спеціальні функції або інша логіка роботи життєвого циклу, які Spring не пропонує за замовчуванням, можеш реалізувати BeanPostProcessor самостійно.

На додаток до зворотних викликів ініціалізації та знищення, об'єкти, керовані Spring, можуть також реалізовувати інтерфейс Lifecycle, щоб ці об'єкти могли брати участь у процесі запуску та завершення, що визначається власним життєвим циклом контейнера.

Інтерфейси зворотного виклику життєвого циклу описані в цьому розділі.

Зворотні виклики ініціалізації

Інтерфейс org.springframework.beans.factory.InitializingBean дозволяє біну виконати ініціалізацію після того, як контейнер встановить усі необхідні властивості біну. Інтерфейс InitializingBean визначає єдиний метод:

void afterPropertiesSet() throws Exception;

Ми не рекомендуємо використовувати інтерфейс InitializingBean, оскільки він занадто пов'язує код зі Spring. Як альтернативу можеш використовувати анотацію @PostConstruct або вказати метод ініціалізації POJO. У разі конфігураційних метаданих на основі XML можна використовувати атрибут init-method для зазначення імені методу, який повертає void і не має аргументів. У конфігурації Java можна використовувати атрибут initMethod у @Bean. Розглянемо наступний приклад:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
Java

public class ExampleBean {
    public void init() {
        // виконуємо певну ініціалізацію
    }
}
Kotlin

class ExampleBean {
    fun init() {
        // виконуємо певну ініціалізацію
    }
}

Попередній приклад дозволяє отримати майже такий самий результат, як і наступний приклад (що складається з двох лістингів):

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
Java

public class AnotherExampleBean implements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        // виконуємо певну ініціалізацію
    }
}
Kotlin

class AnotherExampleBean : InitializingBean {
    override fun afterPropertiesSet() {
        // виконуємо певну ініціалізацію
    }
}

Однак у першому з двох попередніх прикладів код не пов'язаний зі Spring.

Зворотні виклики знищення

Реалізація інтерфейсу org.springframework.beans.factory.DisposableBean дозволяє біну отримати зворотний виклик, коли контейнер, що містить його, знищується. Інтерфейс DisposableBean визначає єдиний метод:

void destroy() throws Exception;

Ми не рекомендуємо використовувати інтерфейс зворотного виклику DisposableBean, оскільки він надмірно пов'язує код зі Spring. Як альтернативу пропонуємо використовувати анотацію @PreDestroy або вказати узагальнений метод, який підтримується визначеннями біна. При використанні метаданих конфігурації на основі XML можна застосовувати атрибут destroy-method на <bean/>. У конфігурації Java можна використовувати атрибут destroyMethod у @Bean. Розглянемо таке визначення:

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
Java

public class ExampleBean {
    public void cleanup() {
        // виконуємо певну роботу зі знищення (наприклад, звільняємо пули підключень)
    }
}
Kotlin

class ExampleBean {
    fun cleanup() {
        // виконуємо певну роботу зі знищення (наприклад, звільняємо пули підключень)
    }
}

Попереднє визначення дає майже такий самий результат, як і наступне:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
Java

public class AnotherExampleBean implements DisposableBean {
    @Override
    public void destroy() {
        // виконуємо певну роботу зі знищення (наприклад, звільняємо пули підключень)
    }
}
Kotlin

class AnotherExampleBean : DisposableBean {
    override fun destroy() {
        // виконуємо певну роботу зі знищення (наприклад, звільняємо пули підключень)
    }
}

Однак перше з двох попередніх визначень не пов'язує код зі Spring.

Ти можеш привласнити атрибуту destroy-method елемента <bean> спеціальне значення (inferred), яке дає команду Spring автоматично виявляти публічний close або shutdown метод для конкретного класу бінів. (Тому підійде будь-який клас, що реалізує java.lang.AutoCloseable або java.io.Closeable). Також можна встановити це спеціальне (inferred) значення для атрибуту default-destroy-method елемента <beans>, щоб застосувати цю логіку роботи до всього набору бінів. Зверни увагу, що це логіка роботи за замовчуванням при використанні конфігурації Java.

Методи ініціалізації та знищення за замовчуванням

Якщо ти пишеш зворотні виклики методів ініціалізації та знищення, які не використовують специфічні для Spring інтерфейси зворотних викликів InitializingBean та DisposableBean, зазвичай ти пишеш методи з такими іменами, як init(), initialize(), dispose() і таке інше. В ідеалі імена таких методів зворотного виклику життєвого циклу стандартизовані в межах проєкту, щоб усі розробники використовували одні й самі методи, що забезпечує узгодженість.

Можна налаштувати контейнер Spring на "пошук" імен методів ініціалізації та зворотного виклику знищення кожного біна, якщо такі імена їм привласнено. Це означає, що ти як розробник додатків можеш писати свої класи додатків та використовувати зворотний виклик ініціалізації під назвою init() без необхідності налаштовувати атрибут init-method="init" для кожного визначення біна. IoC-контейнер Spring викликає цей метод при створенні біна (і відповідно до стандартного контракту зворотного виклику життєвого циклу). Ця функція також забезпечує узгоджену угоду щодо іменування зворотних викликів методів ініціалізації та знищення.

Припустимо, що твої методи зворотного виклику ініціалізації мають ім'я init(), а методи зворотного виклику знищення — destroy(). Тоді твій клас буде схожий на клас у наступному прикладі:

Java

public class DefaultBlogService implements BlogService {
    private BlogDao blogDao;
    public void setBlogDao(BlogDao blogDao) {
        this.blogDao = blogDao;
    }
    // це (що не дивно) метод зворотного виклику ініціалізації
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("The [blogDao] property must be set.");
        }
    }
}
Kotlin

class DefaultBlogService : BlogService {
    private var blogDao: BlogDao? = null
    // це (що не дивно) метод зворотного виклику ініціалізації
    fun init() {
        if (blogDao == null) {
            throw IllegalStateException("The [blogDao] property must be set.")
        }
    }
}

Тому ти можеш використовувати цей клас у бінах, схожих на те, що наведено нижче:


<beans default-init-method="init">
        <bean id="blogService" class="com.something.DefaultBlogService">
            <property name="blogDao" ref="blogDao" />
        </bean>
</beans>

Наявність атрибуту default-init-method в атрибуті елемента верхнього рівня <beans/ > призводить до того, що IoC-контейнер Spring розпізнає метод init класу біна як зворотний виклик методу ініціалізації. Коли бін створюється і компонується, якщо клас біна містить такий метод, він ініціюється у відповідний момент.

Можна налаштувати зворотні виклики методу знищення аналогічним чином (мається на увазі в XML) із використанням атрибуту default-destroy-method в елементі верхнього рівня <beans/>.

У випадках, коли існуючі класи біна вже містять методи зворотного виклику, які названі не відповідно до угоди, можна перевизначити значення за замовчуванням, зазначивши (мається на увазі XML) ім'я методу за допомогою атрибутів init-method та destroy-method самого < bean/>.

Контейнер Spring гарантує, що налаштований зворотний виклик ініціалізації буде викликаний відразу після того, як бін отримає всі залежності. Таким чином, зворотний виклик ініціалізації викликається для посилання на необроблений бін, що означає, що перехоплювачі АОП тощо ще не застосовані до біна. Спочатку цілком створюється цільовий бін, а потім застосовується АОП-проксі (наприклад) зі своїм ланцюжком перехоплювачів. Якщо цільовий бін та проксі визначені окремо, твій код може навіть взаємодіяти з необробленим цільовим біном, минаючи проксі. Отже, було б непослідовно застосовувати перехоплювачі до методу init, оскільки в цьому випадку життєвий цикл цільового біну буде пов'язаний з його проксі або перехоплювачами, що призведе до дивної семантики, коли твій код взаємодіятиме безпосередньо з необробленим цільовим біном.

Об'єднання механізмів життєвого циклу

Починаючи з версії Spring 2.5, є три варіанти управління логікою роботи життєвого циклу біна:

  • Інтерфейси зворотного виклику InitializingBean та DisposableBean

  • Спеціальні методи init() та destroy ()

  • Анотації @PostConstruct та @PreDestroy. Можна об'єднувати ці механізми для керування конкретним біном.

Якщо для біна налаштовано кілька механізмів життєвого циклу, а для кожного механізму налаштоване своє ім'я методу, то кожен налаштований метод запускається у порядку, зазначеному після цієї примітки. Однак, якщо одне й те саме ім'я методу налаштоване — наприклад, init() для методу ініціалізації — для більш ніж одного з цих механізмів життєвого циклу, цей метод запускається один раз, як пояснювалося в попередньому розділі.

Кілька механізмів життєвого циклу, налаштованих для одного й того ж біна, з різними методами ініціалізації, викликаються так:

  1. Методи, анотовані за допомогою @PostConstruct

  2. afterPropertiesSet(), як визначено інтерфейсом зворотного виклику InitializingBean

  3. Спеціальний налаштований метод init()

Методи знищення викликаються в тому ж порядку:

  1. Методи, анотовані за допомогою @PreDestroy

  2. destroy(), як визначено інтерфейсом зворотного виклику DisposableBean

  3. Спеціальний налаштований метод destroy()

Зворотні виклики під час запуску та завершення

Інтерфейс Lifecycle визначає основні методи для будь-якого об'єкта, який має свої власні вимоги до життєвого циклу (наприклад, запуск та зупинка будь-якого фонового процесу):


public interface Lifecycle {
    void start();
    void stop();
    boolean isRunning();
}

Будь-який об'єкт, керований Spring, може реалізувати інтерфейс Lifecycle. Потім, коли сам ApplicationContext отримує сигнали запуску та зупинки (наприклад, для сценарію зупинки/перезапуску під час виконання), він каскадує ці виклики для всіх реалізацій Lifecycle, визначених у цьому контексті. Він робить це шляхом делегування LifecycleProcessor, як показано в наступному лістингу:


public interface LifecycleProcessor extends Lifecycle {
    void onRefresh();
    void onClose();
}

Зверни увагу, що LifecycleProcessor сам є розширенням інтерфейсу Lifecycle. Він також додає два інші методи для реагування на оновлення і закриття контексту.

Заметьте, что привычный интерфейс org.springframework.context.Lifecycle є звичайним контрактом для явних повідомлень про запуск та зупинку і не передбачає автоматичного запуску під час оновлення контексту. Для більш тонкого контролю над автоматичним запуском конкретного біна (включно з фазою запуску), розглянь можливість реалізації org.springframework.context.SmartLifecycle.

Зверни увагу також на те, що повідомлення про зупинку не гарантовано приходять до знищення. У разі звичайного завершення роботи всі біни Lifecycle спершу отримують повідомлення про зупинку, перш ніж поширяться загальні зворотні виклики знищення. Однак у разі гарячого оновлення (hot refresh) протягом існування контексту або у разі зупинених спроб оновлення викликаються лише методи знищення.

Порядок викликів запуску та завершення роботи може відігравати важливу роль. Якщо між будь-якими двома об'єктами існують відносини типу “depends-on”, то залежна сторона починає роботу після своєї залежності та зупиняється перед своєю залежністю. Однак іноді прямі залежності невідомі. Ти можеш знати лише те, що об'єкти певного типу повинні запускатись раніше об'єктів іншого типу. У цих випадках інтерфейс SmartLifecycle визначає інший варіант, а саме метод getPhase(), визначений у його надінтерфейсі Phased. У наступному лістингу показано визначення Phased інтерфейсу:


public interface Phased {
    int getPhase();
}

У наступному лістингу показано визначення SmartLifecycle інтерфейсу:


public interface SmartLifecycle extends Lifecycle, Phased {
    boolean isAutoStartup();
    void stop(Runnable callback);
}

Під час запуску об'єкти з найменшою фазою запускаються першими. У разі зупинки слід діяти у зворотному порядку. Тому об'єкт, який реалізує SmartLifecycle і чий метод getPhase() повертає Integer.MIN_VALUE, буде одним з перших, хто розпочне роботу, і останнім, хто зупиниться. На іншому кінці спектра значення фази Integer.MAX_VALUE вказує на те, що об'єкт повинен розпочати роботу останнім і бути зупинений першим (ймовірно, тому що він залежить від інших процесів, що запущені). При розгляді значення фази також важливо знати, що фаза за замовчуванням для будь-якого "звичайного" об'єкта Lifecycle, який не реалізує SmartLifecycle, дорівнює 0. Тому будь-яке негативне значення фази вказує на те, що об'єкт повинен розпочати роботу до цих стандартних компонентів (і зупинятися після них). Зворотне твердження є правильним для будь-якого позитивного значення фази.

Метод зупинки, визначений SmartLifecycle, приймає зворотний виклик. Будь-яка реалізація має ініціювати метод run() цього зворотного виклику після закінчення процесу завершення цієї реалізації. Це забезпечує асинхронне завершення роботи, коли це необхідно, оскільки реалізація за замовчуванням інтерфейсу LifecycleProcessor, DefaultLifecycleProcessor, чекає протягом вказаного значення часу очікування, поки група об'єктів у кожній фазі не ініціює цей зворотний виклик. За замовчуванням час очікування для кожної фази становить 30 секунд. Можна перевизначити екземпляр процесора життєвого циклу за замовчуванням, визначивши бін з ім'ям lifecycleProcessor у контексті. Якщо потрібно змінити час очікування, достатньо визначити наступне:


<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- значення часу очікування в мілісекундах -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

Як згадувалося раніше, інтерфейс LifecycleProcessor визначає методи зворотного виклику для оновлення та закриття контексту. Останнє керує процесом завершення роботи, якби stop() був викликаний явно, але це відбувається при закритті контексту. З іншого боку, зворотний виклик оновлення (refresh) дозволяє використовувати ще одну особливість бінів SmartLifecycle. Якщо контекст оновлюється (після того, як усі екземпляри об'єктів були створені та ініціалізовані), ініціюється цей зворотний виклик. У цей момент процесор життєвого циклу за замовчуванням перевіряє булеве (логічне) значення, що повертається методом isAutoStartup() кожного об'єкта SmartLifecycle. Якщо true, цей об'єкт запускається в цей момент, а не чекає явного виклику методу start() контексту або свого власного методу (на відміну від оновлення контексту, початок роботи контексту не відбувається автоматично у стандартній реалізації контексту). Значення phase та будь-які відносини типу "depends-on" визначають порядок запуску, як описано раніше.

Поступове завершення роботи IoC-контейнера Spring у не-вебдодатках

Цей розділ стосується тільки не-додатків. Реалізації ApplicationContext для вебдодатків у фреймворку Spring містять код для поступового завершення роботи IoC-контейнера Spring при завершенні роботи відповідної вебпрограми.

Якщо ти використовуєш IoC-контейнер Spring у середовищі програм, відмінних від вебдодатків (наприклад, у повнофункціональному клієнтському десктопному середовищі), зареєструй перехоплювач завершення (shutdown hook) за допомогою JVM. У такий спосіб можна забезпечити поступове завершення роботи та виклик відповідних методів знищення для твоїх бінів-одинаків з метою вивільнення всіх ресурсів. Тобі все одно потрібно належно конфігурувати та реалізувати ці зворотні виклики знищення.

Щоб зареєструвати перехоплювач завершення, виклич метод registerShutdownHook(), який оголошено в інтерфейсі ConfigurableApplicationContext, як показано в прикладі:

Java

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
    public static void main(final String[] args) throws Exception {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        // додаємо перехоплювач завершення вищевказаного контексту...
        ctx.registerShutdownHook();
        // програма запускається...
        // головний метод завершується, перехоплювач викликається перед завершенням роботи програми...
    }
}
Kotlin

import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
    val ctx = ClassPathXmlApplicationContext("beans.xml")
    // додаємо перехоплювач завершення для зазначеного вище контексту...
    ctx.registerShutdownHook()
    // додаток запускається...
    // головний метод завершується, перехоплювач викликається до завершення роботи програми...
}