Как под капотом работает spring

Как писать на Spring в 2017

В одной из классических статей для новичков, мелькавших недавно на Хабре, рассказывалось про создание базового Web приложения на Java. Все начиналось с сервлета, потом создания JSP страницы и, наконец, деплоймента в контейнер. Посмотрев на это свежим взглядом я понял, что для как раз для новичков это, наверняка, выглядит совершенно жутко — на фоне простых и понятных PHP или Node.js, где все просто — написал контроллер, вернул объект, он стал JSON или HTML. Чтобы немного развеять это ощущение, я решил написать «Гайд для новичков в Spring». Цель это статьи — показать, что создание Web приложений на Java, более того — на Spring Framework это не боль и мучительное продирание через web.xml, persistence.xml, beans.xml, и собирание приложения как карточного домика по кусочкам, а вполне себе быстрый и комфортный процесс. Аудитория — начинающие разработчики, разработчики на других языках, ну и те, кто видел Спринг в его не самые лучше времена.

Введение

В этой статье мы посмотрим, что включает в себя современный Спринг, как настроить локальное окружение для разработки Веб приложений, и создадим простое веб-приложение, которое берет данные из БД и отдает HTML страницу и JSON. Как ни странно, большинство статей (на русском языке) для начинающих, которые я нашел в топе поиска описывают и ручное создание контекста, и запуск приложения, и конфигурацию через XML — ничего из этого в современном Спринге делать, разумеется, не обязательно.

Что такое Spring?

Для начала пара слов, что же такое Spring. В настоящее время, под термином «Spring» часто подразумевают целое семейство проектов. В большинстве своем, они развиваются и курируются компанией Pivotal и силами сообщества. Ключевые (но не все) проекты семейства Spring это:

Spring Framework (или Spring Core)
Ядро платформы, предоставляет базовые средства для создания приложений — управление компонентами (бинами, beans), внедрение зависимостей, MVC фреймворк, транзакции, базовый доступ к БД. В основном это низкоуровневые компоненты и абстракции. По сути, неявно используется всеми другими компонентами.

Spring MVC (часть Spring Framework)
Стоит упомянуть отдельно, т.к. мы будем вести речь в основном о веб-приложениях. Оперирует понятиями контроллеров, маппингов запросов, различными HTTP абстракциями и т.п. Со Spring MVC интегрированы нормальные шаблонные движки, типа Thymeleaf, Freemaker, Mustache, плюс есть сторонние интеграции с кучей других. Так что никакого ужаса типа JSP или JSF писать не нужно.

Spring Data
Доступ к данным: реляционные и нереляционные БД, KV хранилища и т.п.

Spring Cloud
Много полезного для микросервисной архитектуры — service discovery, трасировка и диагностика, балансировщики запросов, circuit breaker-ы, роутеры и т.п.

Spring Security
Авторизация и аутентификация, доступ к данным, методам и т.п. OAuth, LDAP, и куча разных провайдеров.

Типичное веб приложение скорее всего будет включать набор вроде Spring MVC, Data, Security. Ниже мы увидим, как это все работает вместе.

Особняком стоит отметить Spring Boot — это вишенка на торте (а некоторые думают, что собственно сам торт), которые позволяет избежать всего ужаса XML конфигурации. Boot позволяет быстро создать и сконфигурить (т.е. настроить зависимости между компонентами) приложение, упаковать его в исполняемый самодостаточный артефакт. Это то связующее звено, которое объединяет вместе набор компонентов в готовое приложение. Пару вещей, которые нужно знать про Spring Boot:

Настройка окружения

Для того, чтобы создать простое приложение, знать, как создать проект Maven с нуля, как настроить плагины, чтобы создать JAR, какие бывают лейауты в JAR, как настроить Surefire для запуска тестов, как установить и запустить локально Tomcat, а уж тем более, как работает DispatcherServlet — совершенно не нужно.

Современное приложение на Spring создается в два шага:

Spring Initializr позволяет «набрать» в свое приложение нужных компонентов, которые потом Spring Boot (он автоматически включен во все проекты, созданные на Initializr) соберет воедино.

В качестве среды разработки подойдет что угодно, например бесплатная IntelliJ IDEA CE прекрасно справляется — просто импортируйте созданный pom.xml (Maven) или build.gradle (Gradle) файл в IDE.

Стоит отдельно отметить компонент Spring Boot который называется DevTools. Он решает проблему цикла локальной разработки, который раньше выглядел как:

В те древние времена даже родилась поговорка, что Spring это DSL для конвертации XML конфигов в стектрейсы.

С включенными Spring Boot DevTools цикл разработки сокращается до:

DevTools будут автоматом проверять изменения в скомпилированном коде или шаблонах, и очень быстро перезапускать (hot reload) только «боевую» часть приложения (как nodemon, если вы знакомы с миром node.js). Более того, DevTools включают интеграцию с Live Reload и после установки расширения в браузере, достаточно скомпилировать проект в IDEA, чтобы он автоматом обновился в браузере.

Разработка

Окей, пора приступать к практической части. Итак, наша цель — создать веб-приложение, которое отдает welcome страницу, обращается с нее же к собственному API, получает JSON с данными из базы и выводит их в таблицу.

Новый проект

Точнее, контейнер нужен — только он предоставлен и настроен Spring Boot-ом — используя Embedded Tomcat

Контроллер

Итак, наш следующий шаг — создать контроллер и вернуть «домашнюю» страницу. Код контроллера выглядит так просто, как и ожидается:

Пара вещей, на которые стоит обратить внимание.

С Котлин это бы выглядело еще лучше и проще, но это потребует введения сразу большого количества новых понятий — язык, фреймворк. Лучше начинать с малого.

Класс, помеченный как @Controller автоматически регистрируется в MVC роутере, а используя аннотации @(Get|Post|Put|Patch)Mapping можно регистрировать разные пути.

Все файлы из каталога resources/static/ считаются статическими, там можно хранить CSS и картинки.

Шаблон

Мы используем Mustache (Handlebar) синтаксис, поэтому шаблон очень напоминает обычный HTML

После компиляции проекта (⌘/Ctrl + F9) — можно сразу идти на http://localhost:8080 и увидеть созданную страницу.

Доступ к базе

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

Предвидя череду комментариев «Как же без геттеров и сеттеров» и «Где же equals / hashCode» — эти элементы упущены сознательно с целью упрощения кода. Совершенно чудовищная ошибка дизайна Java которая заставляет писать эту ерунду (геттеры и методы сравнения), это, конечно, отдельный разговор. Котлин эту проблему, кстати, решает.

Мы снова очень активно используем аннотации — в этот раз из Spring Data (точнее, JPA — это дремучая спецификация для доступа к данным). Этот класс описывает модель с двумя полями, одно из которых генерится автоматически. По этому классу будет автоматически создана модель данных (таблицы) в БД.

Теперь для этой модели пора создать репозиторий. Это еще проще, чем контроллер.

Все, репозиторий можно использовать для работы с базой — читать и писать записи. У внимательного читателя должен сработать WTF детектор — что здесь вообще происходит? Мы определяем интерфейс и внезапно он начинает работать с базой? Все так. Благодаря магии Spring Boot и Spring Data «под капотом» происходит следующее:

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

Теперь можно писать в базу в методе контроллера.

REST контроллер

Следующий шаг — это вернуть все записи из базы в JSON формате, чтобы потом их можно было читать на клиенте.

На что обратить внимание:

Теперь при запросе http://localhost:8080/api/visits (предварительно скомпилировав проект и дав DevTools обновить приложение) мы получим JSON с нужными данными.

Клиентский код

Оставим за рамками этой статьи, пример можно увидеть в исходном коде. Цель этого кода — исключительно продемонстрировать как получить JSON данные с сервера, интеграции с клиентскими фреймворками React, Angular etc намеренно оставлены вне рамок этой статьи.

Тестирование

Spring так же предоставляет мощные средства для Integration и Unit тестирования приложения. Пример кода, который проверяет контроллер:

Используя абстракции типа MockMvc можно легко тестировать внешний интерфейс приложения, в то же время имея доступ к его внутренностям. Например, можно целиком заменить компоненты приложения на моки (заглушки).

Аналогично для API тестов есть набор хелперов для проверки JsonPath выражений.

Тестирование в Spring это все таки отдельная тема, поэтому мы не будем сильно на этом останавливаться сейчас.

Деплоймент

Чтобы собрать и запустить наше приложение в продакшене есть несколько вариантов.

Таким образом сборка и запуск приложения выглядит как:

Для деплоймента этого JAR файла не нужно ничего, кроме установленной Java (JRE). Это так называемый fat JAR — он включает в себя и встроенный сервлет контейнер (Tomcat по умолчанию) и фреймворк, и все библиотеки-зависимости. По сути, он является единственным артефактом деплоймтента — его можно просто копировать на целевой сервер и запускать там.

Более того, файл можно сделать «выполняемым» и запускать его просто из командной строки (Java, конечно, все равно необходима).

На базе этого файла можно легко создать Docker образ или установить его как демон. Больше деталей доступно в официальной документации.

Заключение

Получилось, все же, очень сжато — но уложить даже самый простой вводный курс по Spring в рамки одной статьи не очень просто. Надеюсь, это поможет кому-то сделать первый шаги в Spring-е, и хотя понять его фундаментальные концепции.

Как вы успели заметить, в тексте статьи много раз звучало слово «магия Spring». По сути своей, это очень «магический» фреймворк — даже взглянув на самую верхушку айсберга мы уже видели, что Spring много всего делает в фоне. Это является и плюсом, и минусом фреймворка. Плюс несомненно в том, что многие сложные вещи (очень многие) можно сделать одной аннотацией или зависимостью. Минус же это скрытая сложность — чтобы решить какие-то сложные проблемы, заставить фреймворк работать в крайних случаях или понимать все тонкости и аспекты нужно его неплохо знать.

Чтобы сделать этап «знать» как можно проще, Spring обладает отличной документацией, огромным сообществом, и чистыми исходниками, которые вполне можно читать. Если расположить Spring на шкале Рича Хики, он (Spring) несомненно попадет в easy, но уж точно не simple. Но для современного энтерпрайза (и не только энтерпрайза) он дает невероятные возможности чтобы получить production-ready приложение очень быстро и концентрироваться на логике приложения, а не инфраструктуры вокруг.

Источник

Spring под капотом

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

May 13, 2019 · 17 min read

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

Рассмотрим для пример на основе XML. Остальные будут рассмотрены ниже.

Важное значение имеет XmlBeanDefinitionReader (реализаует интерфейс BeanDefinitionReader). С его помощью происходит настройка контекста. Мы в XML описываем бины, из этого XML поднимаем контекст и с помощью XmlBeanDefinitionReader это все работает, т.е. он является внутренним компонентом Spring: он сканирует XML и все что мы там пишем переводит в BeanDefinitions — это объекты, которые хранят информацию про бины.

Дава й те более подробно остановимся на этом этапе.

В XML мы прописали бины из каких-то классов. Когда поднимается контекст на первом этапе XmlBeanDefinitionReader считывает из XML все декларации бинов.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

А именно, XmlBeanDefinitionReader загружает документ при помощи DefaultDocumentLoader. Каждый элемент документа обрабатывается и если он является бином, то создается BeanDefinition на основе каких-то заполненных данных. Каждый BeanDefinition помещается в Map и хранится в классе DefaultListableBeanFactory.

У интерфейса BeanDefinitions есть много имплементаций.
Основные из них, это:
— AbstractBeanDefinition
— AnnotatedBeanDefinition — если используем аннотацию @Component
— ConfigurationClassBeanDefinition — с помощью него мы можем узнать то, что нам нужно (какого типа и из какого класса создался бин).
— GenericBeanDefinition
— ScannedGenericBeanDefinition

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

Пока бины не созданы, мы имеем доступ к метаданным класса.

Далее отрабатывает BeanFactoryPostProcessor.

Это интерфейс, который позволяет настраивать BeanDefinitions до того, как создаются бины. Он работает на этапе, когда кроме BeanDefinitions еще ничего нету (когда никаких бинов еще нет, ничего нет) и может что-то “подкрутить” в BeanDefinitions.

Он создается через BeanDefinitionRegistryPostProcessor, который при помощи класса ConfigurationClassParser парсирует JavaConfig(например если используем конфигурацию через аннотации) и создает BeanDefinition.

Он содержит единственный метод postProcessorBeanFactory()(который в качестве параметра принимает ConfigurableListableBeanFactory — о ней написано ниже ) у которого есть доступ к BeanFactory, т.е. мы можем как-то повлиять на BeanFactory до того, как он начнет работать и повлиять на BeanDefinitions до того, как BeanFactory из них начнет создавать бины.

BeanFactory — основной ключевой игрок на поле Spring. О нем поговорим чуть позже.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

Далее BeanDefinitionReader кладет бины в map BeanDefinition.

BeanFactory из этих BeanDefinition вначале достает определения этих BeanPostProcessor-ов, создает их и кладет в сторону, т.е. в первую очередь он создаст те бины, которые имплементируют интерфейс BeanPostProcessor и с их помощью будет потом настраивать все остальные бины, которые мы с вами прописали в XML.

BeanFactory — важный элемент структуры, т.к. он отвечает за создание и хранение всех объектов (занимается созданием экземпляров бинов, которые создаются на основе ранее созданных BeanDefinition). Это generic интерфейс. Он изначально был сделан для того, чтобы у разработчика была возможность управлять процессом создания бинов.
После создания BeanDefinition, BeanFactory начинает по ним работать, создает из классов объекты и все бины складывает в контейнер.

BeanPostProcessor — это интерфейс. Он позволяет настраивать бины прежде чем они попадут в контейнер. Здесь задействован паттерн “Change Of Responsibility” (“цепочка обязанностей””).

У этого интерфейса обязательно нужно переопределить 2 метода, если вы собираетесь его имплементировать:
— postProcessBeforeInitialization() — вызывается до init() метода.
— postProcessAfterInitialization() — вызывается после init() метода.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

После того, как объект создался и настроился (т.е. BeanFactory его вначале настроит согласно конфигурации) и до init() метода, BeanFactory сначала передает его BeanPostProcessor-у чтобы он сделал с ним что-нибудь (метод postProcessBeforeInitialization()). Он что-то сделал или ничего не сделал и вернул объект обратно в BeanFactory. Объект прошел первую стадию настройки при помощи BeanPostProcessor.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

Затем вызывается init() метод у данного бина.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

Затем после init() метода идет еще один проход: BeanFactory передает объект BeanPostProcessor-у чтобы он сделал с ним что-нибудь (метод postProcessAfterInitialization()). Он что-то сделал или ничего не сделал и вернул объект обратно в BeanFactory. Объект прошел вторую стадию настройки при помощи BeanPostProcessor.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

В итоге мы получаем полностью настроенные объекты.

Как под капотом работает spring. Смотреть фото Как под капотом работает spring. Смотреть картинку Как под капотом работает spring. Картинка про Как под капотом работает spring. Фото Как под капотом работает spring

В init() методе мы пишем логику, которая инициализирует бин.

На этапе работы конструктора еще ничего не настроено, ничего нету, бин не настроен.

Потому что вначале просканировался XML, затем создались BeanDefinitions, Spring создает бин (при условии что он “singleton”): при помощи Reflection запускается конструктор, конструктор отработал, объект создался. И только когда объект создан Spring может его настроить, т.е. если в конструкторе мы пытаемся обратиться к каким-то вещам, которые должен настроить Spring (ведь их нет еще), то в лучшем случае мы получим NPE.

@PostConstruct — это иными словами init() метод, а на этапе init() метода бин еще не до конца сконфигурирован и поэтому если у бина есть какие-то proxy, аспекты, которые для него что-то делают, этого на этапе @PostConstruct еще не существует. Поэтому нежелательно использовать @PostConstruct для вызова бизнес логики.

По умолчанию XML ничего не знает про аннотации. Про аннотации знают BeanPostProcessor. Аннотацию @PostConstruct (обозначает init() метод) должен обрабатывать какой-то BeanPostProcessor, а именно CommonAnnotationBeanPostProcessor. Для этого в XML файле надо добавить его.

Но если детальней провалиться внутрь, то мы увидим класс CommonAnnotationBeanPostProcessor, который имплементирует интерфейс BeanPostProcessor и отвечает за аннотацию @PostConstruct.

Вы можете написать свой собственный BeanPostProcessor, вам ничто не мешает это сделать. Только не забудьте пометить его как бин (с помощью XML или аннотаций @Component), иначе он у вас не отработает.

Давайте рассмотрим такой пример, в котором вы написали свою аннотацию и хотите чтобы над всеми методами, которые помеченные этой аннотацией, выполнялась какая-то ваша логика.

Вы напишите свой BeanPоstProcessor, который будет относиться к вашей аннотации.
Как я уже упоминал выше, интерфейс BeanPоstProcessor имеет 2 метода, которые вам необходимо переопределить.

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

В java к сожалению (а может и к счастью) нельзя просто взять и подставить новую логику в уже существующий метод. Надо будет сгенерировать новый класс proxy.

Есть важное правило: никто не должен заметить подмены!

2 правила создания:
1. CGLib ( Code Generation Library)— он должен наследовать от оригинального класса и переопределять его методы и добавлять туда свою логику (считается хуже, т.к. мы не от любого класса можем наследовать, также есть такое понятие как final методы и т.д.).
2. Dynamic proxy (JDK) — он должен имплементировать те же самые интерфейсы.

Немного об аннотациях:

Создали свою аннотацию:

@Target(value=ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTestAnnotation <
Class newImplMyTestAnnotation();
>

где newImplMyTestAnnotation — ваша новая имплементация.

Аннотация @Retention позволяет указать жизненный цикл аннотации.

SOURCE — видна только в исходном коде. Когда мы компилируем, в байткоде ничего не будет, в рантайме тоже ничего о ней не знаем.
CLASS — (по умолчанию) видна в скомпилированном файле. Аннотация в байткод попасть должна, но через Reflection в рантайме мы ее считать не сможем, т.е. присутствует только в момент выполнения компилятора.
RUNTIME — видна в процессе выполнения и можно считать через Reflection.

Аннотация @Target указывает, что именно мы можем пометить этой аннотацией: это может быть поле, метод, тип и т.д. Иными словами область ее применения.
ElementType.ANNOTATION_TYPE — наша аннотация может присутствовать только над другими аннотациями
ElementType.CONSTRUCTOR — аннотация может присутствовать только над конструкторами
ElementType.FIELD — аннотация может присутствовать только над полем класса (любым)
ElementType.LOCAL_VARIABLE — аннотация может присутствовать только над локальной переменной метода
ElementType.METHOD — аннотация может присутствовать только над методом
ElementType.PACKAGE — аннотация может присутствовать только над пакетом (будет распространяться на все классы в пакете)
ElementType.PARAMETER — аннотация может присутствовать только над аргументом метода
ElementType.TYPE — аннотация может присутствовать только над классом или интерфейсом

@Inherited — наследуется потомками (аннотация будет распространяться на всех потомков)
@Documented — в Javadoc попадает информация о том, что данный класс помечен данной аннотацией

Когда мы написали свою аннотацию, чтобы она отработала нам надо в XML для нее указать свой BeanPostProcessor, иначе не отработает, т.к. про нее никто не знает.

либо добавить @Component над MyTestAnnotationBeanPostProcessor, указав его как бин.

Для примера мы можем задать смысловую логику аннотации @MyTestAnnotation как Deprecated и в случае ее использования рекомендовать использовать новую имплементацию с какой-то новой логикой. В таком случае указываем новую имплементацию в самой аннотации и пишем свой BeanFactoryPostProcessor, который будет содержать следующий алгоритм:

У BeanFactoryPostProcessor есть PropertySourcesPlaceholderConfigurer, который “проинжектит” значения из property файла в нужные поля класса и в результате создания экземпляра класса вместо @Value(“$”) над полями класса мы увидим готовые значения из файла property.

Также PropertySourcesPlaceholderConfigurer может быть добавлен в цикл настройки созданных BeanDefinition.

Поэтому вытаскивать сами бины неправильно. Надо вытаскивать их BeanDefinitions и уже в них искать ту информацию, которая интересует.

2. пробегаемся по всем BeanDefinitions

3. получаем конкретный BeanDefinition (здесь еще содержится просто метадата, мы можем получить имя класса как String, но не можем получить сам класс, т.к. он еще не загружен)

4. вытаскиваем название (имя) класса из BeanDefinition (из какого класса будет создаваться бин).

5. вытаскиваем класс для BeanDefinitions по имени

6. с помощью beanClass.getAnnotation(MyTestAnnotation.class) достаем аннотацию @MyTestAnnotation

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

public class MyTestAnnotationBeanFactoryPostProcessor implements BeanFactoryPostProcessor <

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) <

Данные махинации опасны для production среды, т.к. мы прописали один бин и на него можем сделать proxy, а в итоге нам придет совершенно другой класс, т.к. наш BeanFactoryPostProcessor изменил BeanDefinitions и заменил их.

Если Spring аспекту нужно сделать proxy на какой-то объект, то он вначале смотрит на его интерфейсы, и если они есть, то использует Dynamic proxy, а если их нету, то CGLib. (С 3.2 версии CGLib идет вместе с Spring)

Т.е., согласно документации, для создания прокси объектов может использоваться как Dynamic Proxy (JDK) так и CGLib, но предпочтение должно отдаваться Dynamic Proxy. И, если класс имеет хотя бы один интерфейс, то именно Dynamic Proxy и будет использоваться (хотя это можно изменить, явно задав флаг proxy-target-class). При создании прокси объекта с помощью Dynamic Proxy на вход передаются все интерфейсы класса и метод для имплементации нового поведения. В результате получаем объект, который абсолютно точно реализует паттерн Proxy. Все это происходит на этапе создания бинов, поэтому, когда начинается внедрение зависимостей, то в реальности внедрен будет этот самый прокси-объект. И все обращения будут производиться именно к нему. Но выполнив свою часть функционала, он обратиться к объекту исходного класса и передаст ему управление.

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

Еще на этапе создания бинов есть то самое “магическое” место, где собственно и производится выбор какую из библиотек использовать. Происходит это в классе DefaultAopProxyFactory, а именно в методе

Другими словами, Spring AOP не просто использует CGLib для создания наследников от классов бинов, а реализует на этом этапе полноценный прокси объект (т.е. объект соответствующий паттерну Proxy). Абсолютно без разницы, какую библиотеку под капотом использует Spring. В любом случае поведение его будет одинаковым. В любом случае для организации сквозного программирования будет создан прокси объект, который собственно и обеспечит вызовы методов объекта реального класса. С другой стороны, если какие-либо методы вызываются из методов этого же класса, то перехватить (перекрыть) средствами Spring AOP их уже не получиться.

Здесь в момент вызова метода A() на самом деле вызывается метод прокси объекта. Сам прокси объект не выполняет вызовов методов целевого объекта, он содержит цепочку интерсепторов, которые делают это. Создается новая транзакция и далее происходит вызов метода B() класса Test. А когда из A() вызовем B(), обращения к прокси нет, вызывается уже сразу метод нашего класса и, соответственно, никаких новых транзакций создаваться не будет.

Вернемся к нашим BPP:

Те BeanPostProcessor-ы, которые в классе что-то меняют, они должны это делать на этапе postProcessAfterInitialization(), а не на этапе postProcessBeforeInitialization().

Нельзя ставить @PostConstruct над методом, который что-то принимает).

Аннотация @PostCоnstruct всегда работает на оригинальный метод до того, как все proxy на него “навесились”.

Если мы заменяем оригинальный объект на proxy до того, как отработал init() метод, то тот механизм в Spring, который ищет аннотацию @PostConstruct и запускает init() метод просто не сможет ее найти, т.к. он уже имеет дело не с оригинальным классом, а с proxy, в котором нет никаких аннотаций.

Dynamic proxy могут перекрывать только public методы. CGLib proxy — кроме public, также и protected методы и package. Соответственно, если мы явно не указали для среза (pointcut) ограничение “только для public методов”, то потенциально можем получить неожиданное поведение. Ну а если есть какие-либо сомнения, можно принудительно включить использование CGLib для генерации прокси-объектов (в последних версиях Spring идет по умолчанию).

Пару слов про CTW и LTW

У нас есть возможность использовать компилятор AspectJ. Т.е. фреймворк написан так, что если мы для описания аспектов используем аннотации @AspectJ, то нам не придется вносить никаких изменений, что бы перейти от runtime weaving к CTW (compile-time weaving).

Для этого нам надо только подключить плагин, который и выполнит компиляцию.

При выполнении этого кода (при условии что мы создали свою аннотацию @MyAnnotation и написали для нее некую логику

при использовании Spring AOP мы получим:

а при использовании CTW:

т.к. связывание кода произошло не только в месте исполнения методов, но и в месте их вызовов. Дело в том, что Spring AOP имеет ограничение, которое позволяет связывать код только по месту исполнения. Возможности AspectJ в этом плане значительно шире.

Также для компилятора AspectJ не нужна аннотация @Component. Но если мы ее уберем, то Spring AOP не найдет такого бина и аспект не будет задействован.

2. LTW (Load-time weaving)

LTW в Spring доступно из коробки. Все что надо сделать — в классе конфигурации добавить еще одну аннотацию: @EnableLoadTimeWeaving

Для этого случая выше написанный код выполнится так:

Для выполнения должен использоваться класс InstrumentationLoadTimeWeaver. В нем метод, который должен вызываться на этапе создания бинов

И в этом случае Spring AOP создает все те же прокси объекты, о которых мы говорили чуть выше. С тем лишь отличием, что связывание теперь будет производиться не на этапе исполнения, а уже на этапе загрузки классов.

Spring AOP является proxy-based фреймворком. Это значит, что всегда будут создаваться прокси-объекты на любой наш бин подпадающий под действие аспекта.

Если нам все-таки надо добиться, что бы в рассматриваемом примере выполнялся вызов одного метода класса другим (с аспектами), мы можем “заинжектить” сервис сам в себя при условии, что бин — singleton (используя последнюю версию Spring):

Не могу обойти стороной транзакции.

Пару слов о том, если объявлена аннотация @Transactional над классом. Более подробно про транзакции в Spring я хочу рассказать в отдельной статье.

Он это сделает после того, как PostConstruct (метод init()) отработал, т.к. транзакции на этапе PostConstruct еще не существуют (она еще просто не настроена).

Напоминаю, у нас есть 3 этапа:
— вначале postProcessBeforeInitialization()
— потом PostConstruct (метод init())
— потом postProcessAfterInitialization() — на этом этапе уже все настроилось!

т.е. другими словами PostConstruct работает до того, как настроились все proxy! (включая те proxy, которые отвечают за транзакции).

Например если метод будет одновременно транзакционными и помечен как @PostConstruct, то он отработает без транзакции!

Т.е. init() метод работает “до” того, как есть proxy, а транзакция как раз имплементируются про помощи proxy.

Если бин “singleton”, то по умолчанию он создается сразу, как только поднимается контекст и складывается в контейнер. Они находятся в контексте Spring, он их кэширует. А все “prototype” создаются в тот момент, когда они нужны. Spring их нигде не хранит.

Если у “singleton” бина есть destroy() метод, то после завершения контекста Spring проходит по всем бинам, которые хранятся в контейнере, находит у них destroy() методы (если они прописаны) и вызывает их и следовательно удалит “singleton” бин. Важно заметить, что у “prototype” не вызовет, потому что Spring нигде его не хранит. Как только кому-то потребовался “prototype” бин, Spring его сделал, отдал вам и забыл про него. Это важно знать!

Немаловажный игрок на нашей арене — это ApplicationListener.

Он умеет слушать контекст Spring, все “events”, которые с ним происходят. Работает на этапе, когда все уже создано. Также он имеет дженерики <>, в которых мы можем указать что конкретно мы хотим слушать. Обозначается аннотацией @EventListener.

— contextStartedEvent — контекст начал свое построение (не построился, а только начал)
— contextRefreshedEvent — когда контекст заканчивает свое построение, он всегда делает refresh.
— contextStoppedEvent
— contextClosedEvent

Какие есть способы настройки Spring:
1) XML
2) Аннотации
3) Java-config
4) Groovy-config

Пару слов про конфигурацию через аннотации:

Чтобы кто-то просканировал пакеты, в которых лежат классы, аннотированные @Component, есть 2 пути:
1. использовать неймспейс: и указываем package, который надо просканировать
2. new AnnotationConfigApplicationContext(“…”);

У него есть 2 поля:

private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;

Затем AnnotatedBeanDefinitionReader начинает работать:
1) регистрирует все @Configuration для дальнейшего парсирования
2) происходит регистрация BeanFactoryPostProcessor (BeanDefinitionRegistryPostProcessor), который парсирует Java-сonfig при помощи класса ConfigurationClassParser и создает BeanDefinition.

Java-config сам по себе является бином.
Дает возможность настраивать наши бины какой-то своей логикой, т.е. мы пишем код, который будет создавать наши объекты. В таком случае нам не нужны BeanFactory и т.д. Используется аннотация @Configuration.

Сканирует пакеты AnnotationConfigApplicationContext, как и в случае с аннотациями.

Java-config парсирует класс AnnotatedBeanDefinitionReader, аналог XMLBeanDefinitionReader для XML.

Когда мы создаем AnnotationConfigApplicationContext и передаем в него 1 или несколько Java-сonfig, то у AnnotationConfigApplicationContext есть внутри AnnotatedBeanDefinitionReader, который делает всю логику: он только регистрирует все Java-сonfig-и а также является частью ApplicationContext.

Java-сonfig — это Java файл и на него при помощи CGLib накручивается proxy и каждый раз, когда нужен бин, происходит делегация в тот метод, который мы пропишем в Java-сonfig.

Пару слов о BeanDefinitions, сделанных через Java-Config.

BeanDefinitions для тех бинов, которые декларируются через Java-config отличаются. В них нету информации о том, из какого класса они сделаны.

т.к. пока не запущен метод никаким образом нельзя узнать что этот метод вернет!
Если метод помечен аннотацией @Bean мы не сможем узнать тип возвращаемого значения.

Когда мы пишем бины в Java-config, мы в основном концентрируемся на том, что говорим Spring при помощи какой логики надо создать бин.

В Groovy-config, XML, аннотациях — все работает через Reflection: Spring сам знает название класса и через Reflection его создает.
В Java-config нет Reflection: Spring сам знает, для того, чтобы создать этот бин надо вызвать этот код. Этот код вернет ему объект и только тогда, когда объект будет получен можно узнать из какого класса он был создан.

В @Bean мы можем указать как будет называться init() метод, destroy() метод, также можем указать id для бина (по умолчанию Spring возьмет имя метода в качестве id бина), но указать из какого класса будет создан бин мы никак не можем!

Когда создался объект и сделав у него getClass(), но это уже происходит на этапе ”после”.

Начиная с версии Spring 4 появилась возможность прописывать бины через Groovy-config.

Создается при помощи new GenericGroovyApplicationContext(“context.groovy”);
Парсируется с помощью GroovyBeanDefinitionReader.

Из существенных минусов можно выделить то, что в нем пока нету таких штуковин, как ComponentScan.

С помощью Java-config мы пишем код, который будет создавать наши объекты.
С помощью Groovy-config мы пишем код, который описывает то, как надо создавать BeanDefinition. Мы никак не можем прописать то, каким образом надо создавать сам бин!

Он считается немного лучше XML тем, что в нем можно писать код, но этот код не относится к созданию объекта.

Пример: Название бина и его имплементация. Просто и лаконично.

coolService(CoolService)
bean.scope = ‘prototype’
bean.initMethod = ‘init’
bean.destroyMethod = ‘closeResources’
>
>

1. ClassPathXmlApplicationContext
2. FileSystemXmlApplicationContext
3. XmlWebApplicationContext
4. AnnotationConfigApplicationContext
5. GenericGroovyApplicationContext
6. StaticApplicationContext

Они отличаются друг от друга тем, каким способом задаются мета-данные и где хранится эта конфигурация.

Например:
— ClassPathXmlApplicationContext — метаданные конфигурируются XML и они лежат в classpath, т. е. в ресурсах модуля
— FileSystemXmlApplicationContext — метаданные тоже конфигурируются XML, но они находятся где-то в файловой системе
— AnnotationConfigApplicationContext — для аннотаций и для Java-Config
— GenericGroovyApplicationContext — для Groovy-Config

Надеюсь данная статья будет полезна.

На написание этой статьи на меня повлияли видео талантливого человека Евгений Борисова. Огромное ему спасибо!

Если вы нашли неточности в описании данной статьи, вы можете написать мне на email и я с радостью вам отвечу.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *