Понимание того, как Spring работает за кулисами - Глава II
Приятно снова вас видеть! Если вы впервые посещаете мой блог на Medium, это вторая часть серии о том, как Spring Framework работает за кулисами. Возможно, вы захотите взглянуть на первую главу, объясняющую основы контекста IoC и Spring:
А теперь давайте перейдем к тому, что сейчас действительно важно. Эта глава будет охватывать больше технических аспектов и даст вам основы для начала написания кода для вашего первого проекта Spring. До сих пор кое-что мы уже знаем:
- В контексте Spring хранятся все объекты, которые будут использоваться позже во время выполнения приложения.
- IoC - это принцип, в котором каждый объект определяет свои зависимости.
- Spring использует аннотации, такие как
@Configuration
и@Bean
, чтобы узнать, какой объект необходимо сохранить.
Итак, вопрос на миллион долларов: как эти сохраненные объекты теперь будут использоваться?
Получение объектов из контекста
Первое, что вам нужно знать, это то, что Spring совсем не волшебный и не знает, где вы собираетесь использовать bean-компоненты, хранящиеся в контексте Spring. Вам нужно сказать ему, где вы хотите сделать этот укол. Давайте использовать более подходящий термин: вам нужно указать Spring, какой объект вы хотите связать с контекстом.
Итак, помня основы Java, первое, что нам нужно, это переменная, представляющая наш желаемый объект:
private StoredBean storedBean;
У этой переменной еще нет экземпляра, поэтому, если вы запустите код, он будет иметь NullPointerException
. Теперь нам нужно связать эту переменную с существующим экземпляром в контексте. Если вы помните последнюю главу, у вас уже должен быть класс @Configuration
, экземпляр которого хранится в контексте как @Bean
:
@Configuration class MyConfigClass { @Bean public StoredBean storedBean() { return new StoredBean() // Here is our instance } }
Итак, теперь, как вы можете себе представить, мы будем использовать еще одну аннотацию для создания связи между нашей созданной переменной и существующим экземпляром в контексте:
@Autowired private StoredBean storedBean;
И теперь у вас есть первая переменная, связанная с экземпляром контекста Spring. Теперь о нескольких вещах, о которых следует помнить:
- Правильный термин - не проводка, это инъекция. Вы должны быть знакомы с настоящим.
- Это только начало, продолжайте читать;)
Что произойдет, если у меня есть две переменные с @Autowired
?
Помня основы Java, у нас есть только один экземпляр в контексте, поэтому обе переменные будут нацелены на один и тот же объект:
Но, как я уже говорил, весна - это вовсе не волшебство. Как Spring идентифицирует совпадающий объект в контексте? Теперь начинается интересное:
Когда вы создаете себя @Bean
, у вас есть 2 варианта:
- Дайте имя на уровне аннотации:
@Bean(name="storedBean") public StoredBean storedBean() { return new StoredBean() // Here is our instance }
- Определите имя на уровне сигнатуры метода (просто
storedBean()
)
В обоих случаях связанное имя в контексте Spring будет storedBean
. Наконец, Spring найдет подходящий экземпляр на основе следующих критериев:
- Соответствующий тип объекта. Ищите переменные того же типа в контексте Spring.
- Ищем
@Qualifier
в случае, если в контексте существуют 2 компонента одного типа. - По имени фасоли.
На примере все понятно, правда? Это очень распространенный сценарий в реальном мире, представьте, что у вас есть приложение с несколькими базами данных:
@Bean public DatabaseConnection databaseA() { return new DatabaseConnection(connectionParams) } @Bean public DatabaseConnection databaseB() { return new DatabaseConnection(otherConnectionParams) }
И имя вашей переменной:
@Autowired private DatabaseConnection database;
Итак, отлаживая процесс, Spring будет:
1- Ищите объект того же типа. У нас есть два экземпляра DatabaseConnection
, поэтому Spring не угадает, какой из них правильный. Следующий шаг.
2- Найдите @Qualifier
имя. Мы его еще не предоставили, так что следующий шаг.
3- Найдите подходящее имя bean-компонента. database
! = databaseA
или databaseB
.
Тогда Spring выдаст исключение: NoUniqueBeanDefinitionException
А теперь давайте исправим эту ошибку. У нас есть 2 варианта:
- Измените имя переменной с
database
наdatabaseA
илиdatabaseB
(в зависимости от того, какое нам нужно) - Более элегантно используйте
@Qualifier
:
@Autowired @Qualifier("databaseA") private DatabaseConnection database;
Теперь Spring будет знать, что нам действительно нужно. Интересно, правда? Продолжайте читать, это еще не все.
Есть третий способ позволить Spring выбрать для вас подходящий боб.
@Profile
аннотация очень полезна, когда вы работаете с разными средами, например,databaseDev
иdatabaseProd
- частые случаи.
Правильное внедрение зависимостей
Как вы можете заметить, до сих пор мы использовали аннотацию @Autowired
только на уровне переменных. Это полезно для академических целей, но может вызвать проблемы в реальных условиях. У нас есть 2 альтернативных способа внедрения зависимостей:
Уровень конструктора
Это наиболее рекомендуемый способ внедрения зависимостей по следующим причинам:
- Правильное применение IoC. Мы определяем зависимости объектов на уровне конструктора, поэтому мы не можем пропустить ни одну из них.
- Легко для тестирования. Вы можете создавать макеты из ваших экземпляров контекста в модульных тестах и внедрять их через конструктор.
private DatabaseConnection database;@Autowired // Constructor level public DatabaseService(DatabaseConnection database) { this.database = database; }
Уровень сеттера
Рекомендуется для необязательных зависимостей. Это связано с тем, что вы не будете в безопасности от создания желаемого объекта без одной или нескольких зависимостей, вместо этого вам нужно сделать это вручную в некоторой части кода:
private DatabaseConnection database;@Autowired // Setter level public setDatabaseConnection(DatabaseConnection database) { this.database = database; }public DatabaseService() { // No dependencies here }
Кроме того, при тестировании вы по-прежнему можете вводить макеты через сеттер.
Итак, зачем прекращать использование инъекций на переменном уровне?
Так как:
- Небезопасно от отсутствия зависимостей
- Действительно сложно проверить. Вы не сможете внедрить какой-либо макет в созданный объект.
Если вы не знакомы с тестированием и имитированием, я рекомендую вам прочитать эту статью, в которой объясняются основы тестирования:
@ Bean scope
Если вы немного знакомы с шаблонами проектирования, возможно, вы уже заметили, что все наши переменные нацелены на один экземпляр. Это шаблон singleton, применяемый в контексте Spring как область видимости bean-компонента, и мы можем изменить его, если он нам понадобится.
singleton
: Область по умолчанию, создающая один экземпляр для всех переменных.prototype
: Создает новый экземпляр при каждой ссылке на компонент.session
: новый экземпляр создается в зависимости от сеанса пользователя (только в веб-среде).request
: новый экземпляр создается в соответствии с сделанным запросом (только в веб-среде)
Например, мы могли бы изменить область видимости нашего bean-компонента databaseA
, просто добавив аннотацию @Scope
:
@Bean @Scope("prototype") public DatabaseConnection databaseA() { return new DatabaseConnection(connectionParams) }
Теперь новый экземпляр DatabaseConnection будет создаваться каждый раз при обращении к databaseA
.
Некоторые заключительные рекомендации для этой главы:
- Читайте о весенних стереотипах:
- Теперь вы знаете, как работает внедрение зависимостей, посмотрите, как внедрить внешнюю конфигурацию из файлов:
Https://www.baeldung.com/spring-value-defaults
Мы закончили вторую часть серии Spring Core, которую я выпускаю в этом месяце. Если вы сочтете это полезным, подпишитесь на меня и следите за обновлениями следующих глав:
- Ядро Spring: IoC и контейнер ✓
- Spring Core: все, что вам нужно знать о внедрении зависимостей ✓
- Spring Core: включение аспектно-ориентированного программирования в свои навыки
- Spring Core: эффективное управление транзакциями
- Ядро Spring: Использование REST и Spring MVC
Кроме того, не стесняйтесь высказывать свои сомнения. Я более чем счастлив помочь вам в этом удивительном путешествии.