Фреймворк Spring є потужним, корисним, а тому й популярним інструментом Java-девелоперів. Але новачкам важко розібратися у великому масиві інформації з безлічі сайтів та книг про функціонал цього рішення. Тож я створив невеликий гайд про базові аспекти роботи зі Spring. Це допоможе новачкам структурувати знання і швидше опанувати цей фреймворк.
Що таке Spring
Spring – це фреймворк, ключовий постулат якого полягає у мінімальному втручанні. Як зазначено на офіційному сайті, Spring спрощує, прискорює і робить безпечнішим програмування на Java. А головне – фреймворк підвищує продуктивність цієї роботи.
Spring об’єднує близько 20 модулів. Охопити їх в межах статті неможливо. Тож я зосередився на ключових Core-модулях. Цього вистачить, щоб зрозуміти сутність Spring як IoC-контейнера, опанувати методи інтеграції фреймворку у застосунок і дізнатися про основні анотації в Spring.
При першому знайомстві з фреймворком багатьох може спантеличити безліч нових термінів. При цьому нерідко у статтях для новачків ці поняття нерозкриті. Автори припускаються, що їх значення вже відомі читачам або зрозумілі з контексту. Але, як на мене, від початку важливо приділити час базовій термінології:
- Фреймворк. Це платформа з набором готових рішень для стандартних завдань з конфігурації програми. Кожен розробник може інтегрувати фреймворк у свій застосунок та зосередитись на основній логіці.
- Залежність. Також це називають Dependency. Це якийсь клас B, необхідний для функціонування якогось класу A.
- Анотація. Це ярлик у вихідному коді, який у своїй роботі використовують компілятор або інтерпретатор. У нашому випадку це робить фреймворк.
- Бін. Словом bean називають клас у Java, який створено відповідно до певних правил. Біни допомагають згрупувати об'єкти, що спрощує передачу даних.
Що таке інверсія контролю та впровадження залежностей
Spring часто називають IoC-контейнером. В цьому є доля істини, але це не розкриває всього функціоналу фреймворку. Тож спочатку треба розібратися із поняттям Inversion of Control (IoC, також інверсія контролю), аби зрозуміти, чому воно важливе. При цьому краще роздивитися все на прикладі програми без використання Spring.
Інверсія контролю – це один із ключових принципів об'єктно-орієнтованого програмування, який сприяє поліпшенню модульності коду і мінімізує залежності між його складовими. Якщо клас A потребує класу B для своєї роботи, він стає залежним від класу B. Тобто один клас не може працювати без іншого. Ця залежність схожа, наприклад, із залежністю автомобіля від двигуна.
Безпосередньо у коді без використання IoC це може виглядати приблизно так, як на ілюстрації. Клас Engine містить метод running, який, припустимо, запускатиме двигун автомобіля. Для активації методу drive спочатку потрібно створити екземпляр класу Engine. Тобто система має переконатися, що з двигуном усе нормально.
У цього підходу є два суттєвих недоліки:
- високий ступінь зв’язаності між класами, що унеможливлює їх незалежну розробку та модифікацію;
- код застосунку в цілому важко змінювати, підтримувати й оновлювати.
Але код із прикладу можна поліпшити. З цим допоможе інтерфейс. Завдяки йому є можливість легко міняти двигун (наприклад, на V8Engine) без зміни в методі drive. Проте все одно є складність в управлінні обраною реалізацією, особливо якщо таких реалізацій багато.
Саме тут і допоможе IoC. З інверсією контролю можна винести створення залежностей із класу. Це позбавляє необхідності кожного разу використовувати оператор new при створенні елементів. Тобто інверсія дає змогу додавати нові залежності ззовні без зміни початкового коду.
У Spring для реалізації IoC найчастіше використовують методи впровадження залежностей (від Dependency Injection, або DI). Але їх можна використовувати й без фреймворку Spring. Приклад такого застосування методу під назвою Setter Injection наведено на наступній ілюстрації. У коді з’являється специфічний метод setEngine, в який передано двигун. Тобто тепер не потрібно самостійно створювати кожен об’єкт класу engine. Достатньо передати його ззовні через метод setEngine.
Також використання залежностей можна виконати через Constructor Injection, коли аргументи передаються через конструктор. Як можна оцінити по ілюстрації, завдяки IoC стає менше залежностей, що спрощує модифікацію та розширення класів.
Але не дивлячись на це, створення класів все одно продовжує вимагати використання оператора new. У цей момент і треба згадати про фреймворк Spring.
Як інтегрувати Spring
У Spring кожен клас називається бін – за аналогією з JavaBeans, які дотримуються таких стандартів, як наявність конструктора без параметрів чи get- і set-методів. І хоча Spring-біни схожі на ті класи, вони не підпорядковані таким суворим правилам.
Для інтеграції Spring у застосунок потрібно виконати такі кроки:
- Розробити конфігурацію для Spring.
- Задати інтерфейс ApplicationContext з відомостями про біни, їх взаємозв’язки й іншими конфігураційними даними.
- Витягти біни з IoC-контейнера.
Як конфігурувати Spring
Існує чимало варіантів конфігурацій, кожен із яких впливає на реалізацію інтерфейсу.
Конфігурація через XML
Код не відрізняється від коду без Spring: це той самий згаданий на початку мінімальний вплив. Тож XML-конфігурація може виглядати так, як на наступній ілюстрації.
На початку коду є безліч покликань. Це – простір імен у Spring. Але більш важливим є інша частина коду, де наведено оголошення бінів:
- engine. Це перший бін, за викликом якого можна отримати, наприклад об’єкт класу V8Engine.
- car. У цьому біні є залежність від engine, яку впроваджено через конструктор (хоча можна зробити це й за допомогою сеттеру).
Також треба створити клас PathXmlApplicationContext та передати йому в аргументи config.xml. Далі залишиться зробити запит до Spring про бін car. Після цього фреймворк автоматично впровадить необхідні залежності. Завдяки конфігурації config.xml Spring розуміє: для car існує залежність від engine. Це звільняє розробника від необхідності вказувати у коді залежність одного класу від іншого.
Конфігурація через анотації
Створювати конфігурації в Spring можна й за допомогою анотацій. Вони допомагають фреймворку зрозуміти, де знаходяться залежності і як з ними працювати. В минулому прикладі можна видалити з config.xml всі згадки про біни, а натомість – додати вказівку на сканування пакета для пошуку анотації @Component.
Після цього Spring автоматично вважатиме біни як класи, позначені анотацією @Component. А місця ін’єкції залежностей треба позначити анотацією @Autowired.
Втім, у версіях від Spring 4.3 можна не застосовувати @Autowired до конструктора. Фреймворк самостійно, за наявності одного конструктора, визначить його як точку застосування залежностей. Хоча при установці анотації на сеттер її потрібно вказувати.
Java-конфігурація
Для реалізації Java-конфігурації у наведеному прикладі треба знову ж видалити файл config.xml та створити конфігурацію через звичайний Java-клас. Цей клас має бути анотований як @Configuration, і всередині нього будуть визначатися потрібні біни.
Головний мінус цього підходу полягає в ручному налаштуванні впровадження залежностей. Однак це також означає, що класи очищені від анотацій і для їх використання достатньо змінити реалізацію ApplicationContext.
Якщо ж обирати між XML та анотаціями, то у кожного підходу є свої переваги:
- Завдяки XML-конфігураціям ви можете розділяти налаштування програми та бізнес-логіку, що може бути зручним. Адже ви виносите конфігурації із коду.
- Анотації роблять конфігурацію наочною, коли ви можете прямо в коді побачити класи як компоненти та залежності між ними.
У будь-якому випадку моделі взаємозамінні. Головне – дотримуватись одного методу впродовж усієї розробки.
Як використати @Autowired для впровадження залежностей
Коли розібралися в інтеграції Spring, можна вже говорити про його ключові функції та вплив фреймворку на застосунок. І перше питання – популярні анотації. Серед них найбільш поширеною є @Autowired. Правда, часто розробники зловживають нею і застосовують не там, де потрібно. Тож ви маєте чітко розуміти її призначення та правила використання.
Анотація @Autowired управляє процесом впровадження залежностей. Вона може бути застосована до будь-якої точки впровадження: конструктора, сеттера, поля. Після цього Spring зможе зв’язати бін із такою точкою. Як приклад на ілюстрації використано впровадження залежностей через сеттер з анотацією @Autowired.
Далі треба створити Java-конфігурацію й анотацію @ComponentScan. Це позбавить від необхідності явно оголошувати біни в коді.
Загалом існує 3 способи Dependency Injection за допомогою @Autowired:
- Впровадження через конструктор. Це найкраще рішення, коли залежність є обов’язковою або має бути незмінною (з використанням ключового слова final). Також завдяки цьому способу легко виявляти класи з дуже великою кількістю залежностей. Якщо залежності впроваджуються через конструктор, то вам одразу помітна велика кількість параметрів, що не є нормальною ситуацією.
- Впровадження через сеттер. Коли залежність не обов’язкова, використання цього способу є кращим варіантом. З ним можна створювати опційні залежності, які впроваджуються повторно. Це добре підходить для класів, які мають часто змінювати конфігурацію. А ще це дозволяє визначати залежності в інтерфейсі. Але ж якщо застосувати його для критичної залежності, це може викликати NullPointerException і краш програми. Тож для мінімізації ризику слід використати анотацію @Required.
- Впровадження через поле. Такий підхід можливий, але його краще оминати. Насамперед цей підхід не дозволяє зробити залежність незмінною. При цьому додавання нових залежностей дуже просте, через що висока вірогідність створення «суперкласів» і перевантаженого коду. Також із таким DI виникає залежність від IoC-контейнеру, що змушує переписувати багато коду при заміні або видаленні контейнеру. Ну, і наостанок, із цим методом є складності з рефлексію при написанні юніт-тестів.
Що стосується вибору між першими способами, це було предметом обговорення навіть серед творців фреймворку. До версії 4.0 вони рекомендували сеттери. Причина: конструктор може стати надто великим за наявності багатьох аргументів (особливо якщо властивості необов’язкові).
Але в останні роки рекомендації змінилися на користь конструкторів. Цей підхід дозволяє створювати незмінні компоненти та гарантувати, що необхідні залежності будуть коректно ініціалізовані. Втім, ви маєте самі обрати краще рішення саме для вашого проєкту – або ж навіть поєднати обидва методи.
Які скоупи можуть бути у бінів
- @Scope("singleton"). У Spring за замовчуванням біни створюються як singleton. Тобто кожен бін має один екземпляр на весь IoC-контейнер. Це забезпечує ефективне використання ресурсів, оскільки на кожен наступний запит на цей бін система буде повертати те саме покликання на уже створений об’єкт.
- @Scope("prototype"). Цей підхід – протилежність singleton. При використанні скоупу prototype система створюватиме екземпляр біну для кожної залежності. Це корисно за необхідності у бінах з різним станом для кожного використання. При цьому залежність із prototype коректно працює лише у компонентах з таким скоупом. Тому є неприпустимим використання prototype-залежностей у singletone-компонентах. Але можна й обійти обмеження – із Method Injection (докладніше читайте тут).
- Скоупи для вебзастосунків. Для вебзастосунків передбачені окремі скоупи, які можуть обмежувати життєвий цикл об’єкта. Це можна налаштувати в межах запиту (request), сесії (session), сервлету (application) і вебсокету (websocket).
Який життєвий цикл біну
Бін не є доступним одразу. Спочатку він проходить через кілька етапів ініціалізації. Для впровадження його життєвого циклу є дві анотації для маркування методу:
- @PostConstruct. Метод буде викликано відразу після конструктора.
- @PreDestroy. Метод буде викликано перед видаленням біну.
Ці методи зручні для додавання чи зменшення ресурсів. Деякі з них діють для singleton, інші – для prototype. При цьому метод @PreDestroy неможливий для prototype-бінів, адже там для кожної залежності створюється окремий об’єкт.
Що таке анотація @Qualifier
У разі множинних реалізацій інтерфейсу, що впроваджується з @Autowired (наприклад, engine, electric engine тощо), виникає помилка. Для усунення цієї неоднозначності використовується анотація @Qualifier. Саме вона вкаже Spring на потрібну реалізацію.
Які компоненти є у @Component
У @Component є аналоги: @Controller, @Service та @Repository. Але вони фактично ідентичні та служать маркерами замість @Component. Однак не виключена поява з часом їх унікальних властивостей. Наприклад, @Repository кілька років тому почав перехоплювати Dao-exceptions для репозиторіїв. Тож краще просто використовувати @Component, якщо ви не впевнені у конкретному використанні цих анотацій.
Додаткові ресурси
Розгляд Spring може бути нескінченним, так як він є великим і містить безліч деталей. Однак не слід забувати: цей фреймворк дійсно спрощує життя. Якщо ви добре розумієте ООП і уважно вивчаєте Spring, ви зможе заощадити багато часу на рутинних завданнях. А якщо виникають якісь питання, на вас завжди чекають корисні матеріали:
Підписуйтеся на ProIT у Telegram, щоб не пропустити жодну публікацію!