В прошлой статье я в двух словах объяснил что такое спринг, что такое бины и контекст. Теперь пришло время попробовать как это все работает.
Я у себя буду делать в Intellij Idea Enterprise Edition. Но все мои примеры должны так же работать и в бесплатной Intellij Idea Community Edition. Просто если увидите на скриншотах, что у меня есть какое-то окно, которого нет у вас — не переживайте, для данного проекта это не критично :)
Для начала создаем пустой мавен проект. Я показывал как это сделать в статье (читать до слов "Пришло время наш мавен-проект превратить в web-проект.", после этого там уже показывается как сделать веб-проект, а этого нам сейчас не нужно)
Создадим в папке src/main/java какой-нибудь пакет (в моем случае я его назвал "
ru.javarush.info.fatfaggy.animals
", вы можете назвать как хотите, просто в нужных местах не забудьте на свое название заменять). И создадим класс Main
в котором сделаем метод
public static void main(String[] args) {
...
}
После чего откроем файл pom.xml и добавим там раздел dependencies
. Теперь идем в мавеновский репозиторий и ищем там spring context последней стабильной версии, и вставляем то, что получили внутрь раздела dependencies
. Чуть более подробно я описал этот процесс в этой статье (см. раздел "Подключение зависимостей в мавене").
Тогда мавен сам найдет и скачает нужные зависимости, и в итоге у вас должно получиться что-то типа такого:
В левом окошке видно структуру проекта с пакетом и классом
Main
.
В среднем окне показано как у меня выглядит pom.xml. Я еще добавил туда раздел properties, в котором указал мавену какая у меня версия джавы используется в исходниках и в какую версию компилировать. Это просто чтобы идея предупреждения мне не кидала при запуске, что используется старая версия джавы. Можете делать, можете нет)
В правом же окошке — видно что хоть мы и подключили только spring context — он автоматически за собой подтянул еще и core, beans, aop и expression. Можно было подключать отдельно каждый модуль, прописывая в помнике для каждого зависимость с явным указанием версии, но нас пока устраивает и такой вариант, как есть сейчас.
Теперь создадим пакет entities
(сущности) и в нем создадим 3 класса: Cat
, Dog
, Parrot
. Пусть у каждого животного будет имя (private String name
, можете захардкодить туда какие-то значения), и геттеры/сеттеры публичные.
Теперь переходим в класс Main
и в методе main()
пишем что-то типа такого:
public static void main(String[] args) {
// создаем пустой спринговый контекст, который будет искать свои бины по аннотациям в указанном пакете
ApplicationContext context =
new AnnotationConfigApplicationContext("ru.javarush.info.fatfaggy.animals.entities");
Cat cat = context.getBean(Cat.class);
Dog dog = (Dog) context.getBean("dog");
Parrot parrot = context.getBean("parrot-kesha", Parrot.class);
System.out.println(cat.getName());
System.out.println(dog.getName());
System.out.println(parrot.getName());
}
Сначала мы создаем объект контекста, и в конструкторе указываем ему имя пакета, которое надо сканировать на наличие в нем бинов. То-есть, спринг пройдется по этому пакету и попробует найти такие классы, которые отмечены специальными аннотациями, дающими спрингу понять, что это — бин. После чего он создает объекты этих классов и помещает их себе в контекст.
После чего мы получаем из этого контекста котика. Обращаясь к объекту контекста — мы просим его дать нам бин (объект), и указываем, какого класса объект нам нужен (тут, кстати, можно указывать не только классы, но и интерфейсы). После чего нам спринг возвращает объект этого класса, который мы уже и сохраняем в переменную.
Далее мы просим спринг достать нам бин, который называется "dog". Когда спринг будет создавать объект класса Dog
— то он даст ему стандартное имя (если явно не указано имя создаваемого бина), которое является названием класса объекта, только с маленькой буквы. Поэтому, поскольку класс у нас называется Dog
, то имя такого бина будет "dog". Если бы у нас там был объект BufferedReader
— то ему спринг дал бы имя по умолчанию "bufferedReader". И поскольку в данном случае (у джавы) нет точной уверенности какого именно класса будет такой объект — то возвращается просто некий Object
, который мы уже потом ручками кастим к нужному нам типу Dog
. Вариант с явным указанием класса удобнее.
Ну и в третьем случае мы получаем бин по классу и по имени. Просто может быть такая ситуация, что в контексте окажется несколько бинов какого-то одного класса, и для того, чтобы указать какой именно бин нам нужен — указываем его имя. Поскольку мы тут тоже явно указали класс — то и кастить нам уже не приходится.
Важно! Если окажется так, что спринг найдет несколько бинов по тем требованиям, что мы ему указали — он не сможет определить какой именно бин нам дать и кинет исключение. Поэтому старайтесь указывать ему максимально точно какой бин вам нужен, чтоб не возникло таких ситуаций.
Если спринг не найдет у себя в контексте вообще ни одного бина по вашим условиям — он тоже кинет исключение.
Ну и далее мы просто выводим имена наших животных на экран чтобы убедиться, что это реально именно те объекты, которые нам нужны.
Но если мы запустим программу сейчас — то увидим, что спринг ругается, что не может найти у себя в контексте нужных нам животных. Так случилось потому, что он не создал эти бины. Как я уже говорил, когда спринг сканирует классы — он ищет "свои" спринговые аннотации там. И если не находит — то и не воспринимает такие классы как те, бины которых ему надо создать.
Чтобы пофиксить это — достаточно просто в классах наших животных добавить аннотацию @Component
перед классом.
@Component
public class Cat {
private String name = "Барсик";
...
}
Но и это не все. Если нам надо явно указать спрингу что бин для этого класса должен иметь какое-то определенное имя — это имя можно указать в скобках после аннотации. Например, чтобы спринг дал нужное нам имя "parrot-kesha
" бину попугайчика, по которому мы в main
-е потом этого попугайчика будем получать — надо сделать примерно так:
@Component("parrot-kesha")
public class Parrot {
private String name = "Кеша";
...
}
В этом вся суть автоматической конфигурации. Вы пишете ваши классы, отмечаете их нужными аннотациями, и указываете спрингу пакет с вашими классами, по которому он идет, ищет аннотации и создает объекты таких классов.
Кстати, спринг будет искать не только аннотации @Component
, но и все остальные аннотации, которые наследуются от этой. Например, @Controller
, @RestController
, @Service
, @Repository
и другие, с которыми мы познакомимся в дальнейших статьях.
Теперь попробуем сделать то же, но используя java-конфигурацию.
Для начала — удалим аннотации @Component
из наших классов. Для усложнения задачи, представим, что это не наши самописные классы, которые мы можем легко модифицировать, добавлять что-то, в том числе и аннотации. А будто эти классы лежат запакованными в какой-то библиотеке. В таком случае мы не можем никак эти классы править чтобы они были восприняты спрингом. Но объекты этих классов нам нужны!
Тут нам пригодится java-конфигурация для создания таких объектов.
Для начала, создадим пакет например configs
, а в нем — обычный джава класс например MyConfig
и пометим его аннотацией @Configuration
@Configuration
public class MyConfig {
}
Теперь нам нужно немножко подправить в методе main()
то, как мы создаем контекст. Мы можем либо напрямую указать там наш класс с конфигурацией:
ApplicationContext context =
new AnnotationConfigApplicationContext(MyConfig.class);
Если у нас несколько разных классов, где мы производим создание бинов и мы хотим подключить сразу несколько из них — просто указываем их там через запятую:
ApplicationContext context =
new AnnotationConfigApplicationContext(MyConfig.class, MyAnotherConfig.class);
Ну и если у нас их слишком много, и мы хотим их подключить сразу все — просто указываем здесь название пакета, в котором они у нас лежат:
ApplicationContext context =
new AnnotationConfigApplicationContext("ru.javarush.info.fatfaggy.animals.configs");
В таком случае спринг пройдется по этому пакету и найдет все классы, которые отмечены аннотацией @Configuration
.
Ну и на случай, если у нас реально большая программа, где конфиги разбиты по разным пакетам — просто указываем название пакетов с конфигами через запятую:
ApplicationContext context =
new AnnotationConfigApplicationContext("ru.javarush.info.fatfaggy.animals.database.configs",
"ru.javarush.info.fatfaggy.animals.root.configs",
"ru.javarush.info.fatfaggy.animals.web.configs");
Ну или название более общего для всех них пакета:
ApplicationContext context =
new AnnotationConfigApplicationContext("ru.javarush.info.fatfaggy.animals");
Можете у себя сделать как хотите, но мне кажется, самый первый вариант, где указывается просто класс с конфигами, подойдет нашей программе лучше всего.
При создании контекста спринг будет искать те классы, которые помечены аннотацией @Configuration
, и создаст объекты этих классов у себя. После чего он попытается вызывать методы в этих классах, которые помечены аннотацией @Bean
, что значит, что такие методы будут возвращать бины (объекты), которые он уже поместит себе в контекст.
Ну что ж, теперь создадим бины котика, собачки и попугайчика в нашем классе с java-конфигурацией. Делается это довольно просто:
@Bean
public Cat getCat() {
return new Cat();
}
Получается, что мы тут сами вручную создали нашего котика и дали спрингу, а он уже поместил этот наш объект к себе в контекст. Поскольку мы явно не указывали имя нашего бина — то спринг даст бину такое же имя, как и название метода. В нашем случает, бин кота будет иметь имя "getCat
". Но так как в main
-е мы все-равно получаем кота не по имени, а по классу — то в данном случае нам имя этого бина не важно.
Аналогично сделайте и бин с собачкой, но учтите, что спринг назовет такой бин по названию метода.
Чтобы явно задать имя нашему бину с попугайчиком просто указываем его имя в скобках после аннотации @Bean
:
@Bean("parrot-kesha")
public Object weNeedMoreParrots() {
return new Parrot();
}
Как видно, тут я указал тип возвращаемого значения Object
, а метод назвал вообще как угодно. На название бина это никак не влияет потому что мы его явно тут задаем. Но лучше все-таки тип возвращаемого значения и имя метода указывать не "с потолка", а более-менее понятно. Просто даже для самих себя, когда через год откроете этот проект. :)
Тепер рассмотрим ситуацию, когда для создания одного бина нам нужно использовать другой бин.
Например, мы хотим чтобы имя кота в бине кота состояло из имени попугайчика и строки "-killer". Без проблем!
@Bean
public Cat getCat(Parrot parrot) {
Cat cat = new Cat();
cat.setName(parrot.getName() + "-killer");
return cat;
}
Тут спринг увидит, что перед тем, как создавать этот бин — ему понадобится сюда передать уже созданный бин попугайчика. Поэтому он выстроит цепочку вызовов наших методов так, чтобы сначала вызвался метод по созданию попугайчика, а потом уже передаст этого попугайчика в метод по созданию кота. Тут сработала та штука, которая называется dependency injection: спринг сам передал нужный бин попугайчика в наш метод. Если идея будет ругаться на переменную parrot
– не забудьте изменить тип возвращаемого значения в методе по созданию попугайчика с Object
на Parrot
.
Кроме того, джава-конфигурирование позволяет выполнять абсолютно любой джава-код в методах по созданию бинов. Можно делать реально что угодно: создавать другие вспомогательные объекты, вызывать любые другие методы, даже не помеченные спринговыми анотациями, делать циклы, условия - что только в голову придет!
Этого всего при помощи автоматической конфигурации, и уж тем-более использованием xml-конфигов — не добиться.
Теперь рассмотрим задачку повеселее. С полиморфизмом и интерфейсами :)
Создадим интерфейс WeekDay
, и создадим 7 классов, которые бы имплементили этот интерфейс: Monday
, Tuesday
, Wednesday
, Thursday
, Friday
, Saturday
, Sunday
.
Создадим в интерфейсе метод String getWeekDayName()
, который возвращал бы название дня недели соответствующего класса. То-есть, класс Monday
возвращал бы "monday
", итд.
Допустим, стоит задача при запуске нашего приложения поместить в контекст такой бин, который бы соответствовал текущему дню недели. Не все бины всех классов, которые имплементят WeekDay
интерфейс, а только нужный нам. Это можно сделать примерно так:
@Bean
public WeekDay getDay() {
DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
switch (dayOfWeek) {
case MONDAY: return new Monday();
case TUESDAY: return new Tuesday();
case WEDNESDAY: return new Wednesday();
case THURSDAY: return new Thursday();
case FRIDAY: return new Friday();
case SATURDAY: return new Saturday();
default: return new Sunday();
}
}
Тут тип возвращаемого значения — это наш интерфейс, а возвращаются методом реальные объекты класов-реализаций интерфейса в зависимости от текущего дня недели.
Теперь в методе main()
мы можем сделать так:
WeekDay weekDay = context.getBean(WeekDay.class);
System.out.println("It's " + weekDay.getWeekDayName() + " today!");
Мне выдало, что сегодня воскресенье :) Уверен, что если я запущу программу завтра — в контексте окажется совсем другой объект.
Обратите внимание, тут мы получаем бин просто по интерфейсу: context.getBean(WeekDay.class)
. Спринг посмотрит в своем контексте какой из бинов у него там имплементит такой интерфейс — его и вернет. Ну а дальше уже получается, что в переменной типа WeekDay
оказался объект типа Sunday
, и начинается уже знакомый всем нам полиморфизм, при работе с этой переменной. :)
И пару слов про комбинированный подход, где часть бинов создается спрингом самостоятельно, используя сканирование пакетов на наличие классов с аннотацией @Component
, а некоторые другие бины — создаются уже используя java-конфиг.
Для этого вернемся к первоначальному варианту, когда классы Cat
, Dog
и Parrot
были отмечены аннотацией @Component
.
Допустим, мы хотим создать бины наших животных при помощи автоматического сканирования пакета entities
спрингом, а вот бин с днем недели создавать так, как мы только-что сделали.
Все что надо сделать — это добавить на уровне класса MyConfig
, который мы указываем при создании контекста в main
-е аннотацию @ComponentScan
, и указать в скобочках пакет, который надо просканировать и создать бины нужных классов автоматически:
@Configuration
@ComponentScan("ru.javarush.info.fatfaggy.animals.entities")
public class MyConfig {
@Bean
public WeekDay getDay() {
DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
switch (dayOfWeek) {
case MONDAY: return new Monday();
case TUESDAY: return new Tuesday();
case WEDNESDAY: return new Wednesday();
case THURSDAY: return new Thursday();
case FRIDAY: return new Friday();
case SATURDAY: return new Saturday();
default: return new Sunday();
}
}
}
Получается, что при создании контекста спринг видит, что ему нужно обработать класс MyConfig
. Заходит в него и видит, что нужно просканировать пакет "ru.javarush.info.fatfaggy.animals.entities
" и создать бины тех класов, после чего выполняет метод getDay()
из класса MyConfig
и добавляет бин типа WeekDay
себе в контекст. В методе main()
мы теперь имеем доступ ко всем нужным нам бинам: и к объектам животных, и к бину с днем недели.
Как сделать так, чтобы спринг подхватил еще и какие-то xml-конфиги - нагуглите в интернете самостоятельно уже если понадобится :)
Резюме:
- стараться использовать автоматическую конфигурацию;
- при автоматической конфигурации указываем имя пакета, где лежат классы, бины которых надо создать;
- такие классы помечаются аннотацией
@Component;
- спринг проходит по всем таким классам и создает их объекты и помещает себе в контекст;
- если автоматическая конфиграция нам по каким-то причинам не подходит — используем java-конфигурирование;
- в таком случае создаем обычный джава класс, методы которого будут возвращать нужные нам объекты, и помечаем такой класс аннотацией
@Configuration
на случай, если будем сканировать весь пакет целиком, а не указывать конкретный класс с конфигурацией при создании контекста; - методы этого класса, которые возвращают бины — помечаем аннотацией
@Bean
;
@ComponentScan
.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ