Просте spring додаток з використанням анотації @autowired

У минулому пості коли ми створювали просте spring додаток. для зв'язування компонентів ми використовували конфігураційний xml файл. Але цей підхід з xml файлом заснований на old style Spring тобто на Спрінг до аннатаціонних часів. У цьому пості піднімемо таке ж додаток, тільки для зв'язування бінов будемо використовувати аннтоаціі, точніше анотацію @Autowired. Всю вступну теорію по Спрінг як зміг розкрив в минулому пості. в цьому буду вказувати тільки на відмінності від підходу створення спрінгового додатки яке зв'язується з допомогою конфігураційного xml файлу від спрінгового додатки, яке зв'язується анотацією @Autowired.

Так як в цьому пості я детально розповідаю про спринг, тому він вийшов трошки довгий, хто хоче опис по коротше і перейти відразу до справи, то може зазирнути в пост Spring в двох словах

Для цього прикладу нам знадобиться:

Готовий проект простого спринг додатки можна скачати за посиланням:

Стурктура проекту

Після того як ми додамо всі необхідні файли в додаток і запустимо build.sh скрипт, в поточному каталозі з'явиться каталог build. зі структурою зібраного застосування:

Old spring style

Спочатку наведу приклад простого спринг додатки з предудщего поста, коли для зв'язування компонентів ми користувалися xml файлом:

Figure.java

Circle.java

Rectangle.java

Print.java

Execute.java

анотація @Autowired

Всі класи точно такі ж як і в попередньому пості просте spring додаток. У цьому пості ми їх трохи змінимо, точніше дещо додамо. Але спочатку ще раз подивимося на конфігураційний файл context.xml з минулого поста, коли ми піднімали просте spring додаток. Звернемо увагу на двадцяту сходинку:

context.xml

У цьому рядку бін circle зв'язується з біном print. Для того щоб бін circle зв'язати з біном print. ми встановлюємо бін circle для властивості figure біна print. Це означає, що якщо ми видалимо цей рядок зв'язування, то бін print залишиться без кола і виводити буде нічого. Щоб бін cirlce все таки підперезувався до біну print можна вчинити інакше, проаннотіруем поле Figure анотацією @Autowired. а тег

(Тобто двадцяту сходинку) викинемо з конфігураційного файлу, тобто на даний момент клас Print.java виглядати так (з доданою анотацією @Autowired в сьомому рядку):

Print.java

а конфігураційний файл context.xml виглядає так:

context.xml

BeanPostProcessor, кастомниє і некастомние (спрінговие) біни

У попередньому файлі конфігурації context.xml реєструється три біна, це: circle. rectangle і print. Зауважимо, що зв'язування компонентів нікуди не поділося, воно просто перенесено з конфігураційного файлу в клас Print.java і тепер здійснюється через анотацію @Autowired (рядок 7 в попередньому лістингу Print.java). Так це? Незнаю, давайте подивимося. Збираючись проект командою ./build.sh:

І що ж вийшло? З виведення бачимо, що біни то створилися, але при спробі виклику методу showSquare () вилетіла NPE.
Якщо придивитися до цієї помилку, то можна припустити, що в методі showSquare () здійснюється спроба разрезолвіть покажчик на null. Так воно і відбувається, в методі showSquare (). здійснюється звернення до полю figure. але воно порожнє. Це може означати те, що бін circle НЕ автоподвязался до біну print. Але а в чому справа? Хіба анотації @Autowired мало? Виходить, що так. Справа в тому, що ми то вказали анотацію @Autowired. але Спірінг про це нічого не знає, тому щоб біни автоматично підв'язати, потрібно спринг поставити до відома про це. Ставиться спринг до відома про анотації @Autowired додаванням в контекст біна

У подальших міркуваннях будемо вважати, що біни придумані і піднімаються нами це кастомниє біни, а біни з спрінговой бібліотеки спрінговие. Що ми щас зробили? Ми додали спрінговий бін (саме спрінговий, що не кастомний) AutowiredAnnotationBeanPostProcessor який реалізує інтерфейс BeanPostProcessor і ці біни спринг створює в першу чергу, тому що це біни, які налаштовують інші біни. Тобто спочатку спринг піднімає все біни які реалізують інтерфейс BeanPostProcessor. а потім, в момент створення і налаштування Спрінг нашого (кастомними) біна, BeanFactory (BeanFactory це фабрика яка створює біни) створює кастомний бін, і потім він (спринг) передає його (кастомний бін) на обробку в бін який реалізує інтерфейс BeanPostProcessor в нашому випадку це в бін AutowiredAnnotationBeanPostProcessor (спрінговий бін). Ще раз нагадую бін AutowiredAnnotationBeanPostProcessor вже був попередньо створений Спрінг до того, як він (спринг) створив кастомний бін. У AutowiredAnnotationBeanPostProcessor реалізована логіка яка вишукує все анотовані поля анотацією @Autowired і через рефлекшен кладе в них щось, наприклад інший бін. Потім, після обробки кастомними біна бін пост процесором, оброблений бін (вже з просіканими полями) повертається Спрінг (а точніше в BeanFactory), і спринг, вже повністю готовий бін, кладе в контейнер.

Додали ми спрінговий бін AutowiredAnnotationBeanPostProcessor. але він обробляє тільки анотацію @Autowired. що якщо в класі у нас присутні інші анотації, наприклад @Required або ще які-небудь? Все те ж саме, якщо це анотація @Required. то її обробляє бін пост процесор org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor питання тільки скільки треба знати різних бін пост процесорів щоб їх включити в контекст для кожної анотації? Щоб не довелося всіх їх запам'ятовувати і додавати в контекст ми можемо використовувати неймспейс context: annotation-config який все це буде робити за нас. Замість, того щоб писати всі ці довгі імена бін пост процесорів в конфігурації, можна написати:

Під цим тегом прячются іксемель теги які додають в контекст всі необхідні бін пост процесори, для всіх спрінгових анотацій.
Додайте цей тег в конфігураційний файл. Ось так повинен виглядати конфігураційний файл:

context.xml

Запускаємо ще раз збірку проекту командою ./build.sh. бачимо що біни піднімаються, але знову вилітає ексепшен, на цей раз BeanCreationException:

Ну що знову не так? За вивчивши трохи логи, можна звернути увагу на це повідомлення:

Тобто спринг нам говорить про те, що він не знає з яким біном зв'язати поле com.devblogs.component.figure.Figure з circle або з rectangle. Все було б добре, якби у нас був тільки один бін типу Figure. але у нас їх два, обидва є чаілдамі типу Figure. Спрінг на даний момент не знає, з яким біном клас Print пов'язувати, тому спринг не запуститься. Щоб спринг зміг зв'язати клас Print з яким-небудь біном можна прибрати з конфігураційного файлу реєстрацію якогось одного з бінов або circle або rectangle. У цьому випадку один з них перестане бути біном, а той бін який залишиться стане єдиним біном який успадковується від Figure і тоді все запрацює. Але нас такий варіант не влаштовує, тому що ми хочемо щоб у нас було обидва біна, а підперезувався до полю Figure тільки один з них. Щоб спринг не напружує який бін йому вибрати, ми повинні йому сказати, який саме бін покласти в поле Figure. Для цього використовується анотація @Qualifier. яка говорить Спрінг який бін треба покласти йому в це поле. Додайте в клас Print.java анотацію @Qualifier разом з назвою Біна, який вказується в дужках, з яким спринг буде пов'язувати поле Figure. Тепер клас Print.java повинен виглядати так:

Print.java

Тут ми говоримо Спрінг за допомогою анотації @Qualifier (рядок 9), що бін circle потрібно покласти в бін print. Крім того зверніть увагу, що в класі Print.java тепер відсутній сет метод для поля Figure. Його немає тому що, тепер спринг просечівает поле figure через рефлексію.
Після того, як ми все це справа заздалегідь командою ./build.sh все повинно відпрацювати без помилок, і у висновку ми повинні побачити площа кола, тому що покажчик figure в методі showSquare () нарешті знайшов в пам'яті об'єкт Circle:


Теж саме що і тег

тобто крім додавання всіх необхідних бін пост процесорів в контекст, він ще просканує пакети на наявність анотації @Component або @Service.
Тег context: component-scan сканує певний пакет або певні пакети і шукає в ньому біни і ті, що знайде створює. Що це означає? Це означає, що в файлі конфігурації ми можемо опустити явне визначення бінов переклавши цю процедуру на спринг, в тому числі тих бінов, які вимагають параметри конструктора, але про це трохи далі, а поки параметри конструктів бінов circle і rectangle будемо ініціювати через конфігураційний файл, а ось визначення біна print ми можемо прибрати з конфігураційного файлу. Видаліть визначення біна print з конфігураційного файлу context.xml і наш конфігураційний файл повинен виглядати так:

context.xml

Тепер конфігураційний файл context.xml складається тільки з двох визначень бінов і з одного тега context: component-scan.
А тепер зі зміненим конфігураційним файлом, запустимо білд скрпіт build.sh:

З попереднього висновку видно, що два біна circle і rectangle створилися успішно, а ось на етапі створення біна print. якраз того самого, який ми викинули з конфігураційного файлу, додаток зафейлілось 🙁. Ексепшен NoSuchBeanDefinitionException говорить про те, що спринг не зміг знайти бін print в своєму контексті, а не зміг бо він не був створений. Але що це означає? Трохи вище я писав, що якщо викинути визначення біна з конфігураційного файлу, то він перестає бути біном. Ось це і сталося. Ну добре, а як же тег

Хіба він не змусив спринг про сканував пакет com.devblogs.component (і все його під пакети) на біни? Відповідь спринг просканував пакети, але він там нічого не знайшов. Чому? Давайте розбиратися далі.

Анотації @Service і @Component

Спрінг не виявлено бінов в пакетах бо на думку спрінга (а думка спрінга це остання інстанція) їх там немає. Давайте ще раз подивимося на клас Print.java (я розумію ми на нього вже багато раз за сьогодні дивилися, і все таки ще раз):

Print.java

Можна з нього зрозуміти що це бін або компонент спрінга? Ось і спринг не може, а то що там є анотації @Autowired і @Qualifier це ще не факт, що це компонент. Спрінг потрібні гарантії що це компонент. Для цього є анотація, яка так і називається @Component або @Service. Все що нам потрібно зробити, це додати анотацію @Component перед назвою класу.

Відмінність анотації @Service від анотації @Component

Анотації @Service і @Component взаємозамінні. Обидві анотації кажуть Спрінг, що клас, над якими вони проставлені є біном, тобто кандидатом на автоматичне виявлення якщо в файлі конфігурації проставлений тег context: annotation-config і тег автоматичного сканування context: component-scan або тільки тег context: component-scan. Відмінність між ними лише в ідеї використання анотованого класу. Анотацію @Service краще застосовувати до біну, який надає службу іншим бінам. Анотація @Component найкраще підходить для бінов-властивостей. тобто тих бінов які надають значення властивостей іншим бінам, а не виконують код, результати якого використовують інші біни. Але це всього лише рекомендації, жорстко дотримуватися цих рекомендацій ніхто не змушує і в подальших прикладах буде використовуватися в основному анотація @Component. навіть якщо клас надає бізнес логіку. [102]


Анотація @Component маркує клас як компонент для спрінга. Додайте цю анотацію на самому початку перед ім'ям клас, щоб клас Print.java виглядав ось так:

Print.java

Тепер спробуємо ще раз зібрати і заздалегідь додаток командою ./build.sh:

На цей раз додаток ранитися без помилок. Тепер про те, що на цьому етапі відбувається. Після того, як спринг знайшов в файлі конфігурації тег context: component-scan. він починає процес пошуку бінов, заглядаючи в кожен клас в межах того пакету, який вказаний в тезі. Припустимо спринг заглядає в клас Print.java. в ньому він знаходить анотацію @Component. і робить висновок, що це бін. Потім створює його інстанси і пов'язує з ним бін circle який був створений ручним способом в файлі конфігурації context.xml. По суті, якби не параметри конструктора, то можна було б в файлі конфігурації взагалі нічого не визначати, а тільки лише додати один тег

і він би все зробив. Більш того, навіть ініціалізацію параметрів конструктора можна перенести з конфігураційного файлу в клас як ми це робили для ініціалізації властивості figure.

анотація @Value

Цей розділ буде як доповнення до попереднього, на практиці ініціалізація параметрів всередині класів проводиться ніколи, цей розділ швидше присвячений як можна робити, але як не потрібно робити. тому відразу переходите від сюди до збірки проекту.
Для того щоб перенести ініціалізацію параметрів конструктора (або методу) з конфігураційного файлу в клас для цього використовуються дві анотації, одна вже нам знайома @Autowired. а інша @Value. Тепер давайте викинемо з конфігураційного файлу context.xml залишилися визначення бінов:

і залишимо тільки тег

тепер конфігураційний файл context.xml складається тільки з одного тега context: component-scan:

context.xml

Так як файл конфігурації спрощується до одного рядка

Але і це ще не все. Можна взагалі обійтися без конфігураційного файлу, а скористатися об'єктом AnnotationConfigApplicationContext який створюється в меін методі додатки. В цей об'єкт передається пакет з якого починається сканувати класів на наявність анотацій @Component і можна з нього витягати біни. В цьому випадку конфігураційний файл context.xml стає вже непотрібним, так як все що там йдеться робити, вже робиться об'єктом AnnotationConfigApplicationContext. тобто пошук і створення бінов.
А в java класах Circle.java і Rectangle.java треба про анотувати формальні параметри конструктора анотацією @Value вказуючи в круглих дужках значення, яке ми хочемо передати в конструктор, а сам конструктор про анотувати анотацією @Autowired. а так як ми ще викинули визначення бінов circle і rectangle з конфігураційного файлу context.xml. то треба ще про анотувати ці класи інструкцією @Component. Змінені класи Circle.java і Rectangle.java повинні виглядати так:

Circle.java

Rectangle.java

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

Збираємо всі разом

Ну ось і все, тепер наведу ще раз все класи з конфігураційних файлів після всіх зроблених змін (зміни підсвічені):

Figure.java

Circle.java

Rectangle.java