JavaRush/Java блог/Java Developer/Spring для ленивых. Основы, базовые концепции и примеры с...
Стас Пасинков
26 уровень

Spring для ленивых. Основы, базовые концепции и примеры с кодом. Часть 2

Статья из группы Java Developer
участников
В прошлой статье я в двух словах объяснил что такое спринг, что такое бины и контекст. Теперь пришло время попробовать как это все работает. Spring для ленивых. Основы, базовые концепции и примеры с кодом. Часть 2 - 1Я у себя буду делать в 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. Чуть более подробно я описал этот процесс в этой статье (см. раздел "Подключение зависимостей в мавене"). Тогда мавен сам найдет и скачает нужные зависимости, и в итоге у вас должно получиться что-то типа такого:
Spring для ленивых. Основы, базовые концепции и примеры с кодом. Часть 2 - 2
В левом окошке видно структуру проекта с пакетом и классом 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;
  • если хотим подключить возможность автоматического сканирования при использовании java-конфигурации — используем аннотацию @ComponentScan.
Если ничего не понятно — то попробуйте прочитать эту статью через пару дней. Ну или если вы на ранних уровнях джавараша, то возможно, что спринг для вас пока немного рановато изучать. Вы всегда сможете вернуться к этой статье чуть позже, когда будете чувствовать себя уже более уверенней в программировании на java. Если все понятно — можете попробовать перевести какой-нибудь свой pet-проект на спринг :) Если что-то понятно, а что-то не очень - прошу в комменты :) Туда же и предложения и замечания, если я где-то ступил или написал какую-то глупость) В следующей статье мы резко нырнем в spring-web-mvc и сделаем простенькое веб-приложение, используя спринг.
Комментарии (123)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
ultraheko
Уровень 1
19 марта, 16:18
В следующей статье мы резко нырнем в spring-web-mvc и сделаем простенькое веб-приложение, используя спринг. Подскажите где можно ознакомиться со следующей статьей?
Стас Пасинков Software Developer в Zipy Master
19 марта, 23:38
ніде я її не писав. пошукайте інші статті в інтернеті, зараз вже мають бути або почитайте книгу, це ще краще
Владислав
Уровень 82
Expert
20 января, 12:41
Week week = context.getBean(Week.class); System.out.println("It s " + week.getWeekDayName()); объясните откуда метод getWeekDayName знает какой день недели. Если Всю логику выполняет метод конфига
Стас Пасинков Software Developer в Zipy Master
20 января, 23:12
метод getWeekDayName() вертає лише ім'я/назву дня тижня сам же об'єкт/бін дня тижня генерується підчас конфігурації апкі, да
Владислав
Уровень 82
Expert
19 января, 07:21
хоть я сейчас только на половине статьи, но у вас шикарная подача материала. Спасибо за ваши труды и хочу видеть от вас больше полезной информации на сложноватые для новичков темы.
Alexey
Уровень 4
14 ноября 2022, 00:33
что-то скриншот из идеи совсем плох
Стас Пасинков Software Developer в Zipy Master
14 ноября 2022, 08:49
заблюреный)) так даже лучше)) человек не будет всматриваться в что именно написано, а просто увидит структуру)) не знаю, заливал туда я нормальный скриншот. то наверное сервера джавараша провели "оптимизацию" картинок. ведь заливал я его туда много лет назад, да еще и на другом сервере, а сюда они уже сами ее переносили вроде)
Владимир
Уровень 1
20 июля 2022, 11:14
Понял! Надо из MyConfig убрать объявление просто кота.
Стас Пасинков Software Developer в Zipy Master
20 июля 2022, 13:59
ну если у вас было два бина кота с одинаковым именем - тогда да, будет ошибка. я думал просто изменить существующий бин кота с обычного, на "убийцу". но в принципе, можно было просто добавить еще один бин кота, просто дать ему другое имя. тогда оба кота были бы в распоряжении спринга
Владимир
Уровень 1
20 июля 2022, 09:49
Я не понял, как создавать кота-убийцу. Т. е. как коту передать попугая?
Стас Пасинков Software Developer в Zipy Master
20 июля 2022, 13:58
там не надо ничего передавать вручную) надо просто "попросить" спринг об этом) то-есть, указать, что вашему методу нужен такой-то бин в качестве параметра. а спринг уже сам все передаст)
Nik безработный
15 июля 2022, 08:03
Может это я такой тупой, но, вместо того, чтобы за полчаса ознакомиться с материалом, мне надо пол дня разбираться, что недосказано и недописано, чтобы эти примеры заработали! И так у всех авторов!
Стас Пасинков Software Developer в Zipy Master
15 июля 2022, 09:32
что именно было недописано и недосказано?
GamGas
Уровень 2
15 июля 2022, 13:59
Ну, вызов из контекста по строке "dog" не возвращал бин не смотря на то, что был прописан в MyConfig, так что пришлось явно указать "компонент". Хотя, если вызов будет идти через .class то все нормально. Как я понял, бин возвращает явный тип Dog, а запрос по строке типа (Dog) context.getBean("dog"); выполняет приведение типа уже после запроса. Так что спринг не может найти запрос dog непосредственно в виде объекта. Проблема решается явным указанием имени бина в виде @Bean("dog"), но это теряет смысл при автомаппинге.
Стас Пасинков Software Developer в Zipy Master
15 июля 2022, 21:14
я так понял, проблема была именно с объектом собаки? а с котом все ок было, судя по всему... дело в том, что я не показывал в примерах как настроить бин собаки. по идее, читатель должен был это сделать по аналогии как и с бином кота. вот цитата из статьи: Аналогично сделайте и бин с собачкой, но учтите, что спринг назовет такой бин по названию метода. возможно, было указано другое имя метода, не знаю. в примере я указал
Dog dog = (Dog) context.getBean("dog");
все верно, тут мы приводим результат вызова метода getBean() к нужному нам типу (Dog). это необходимо потому, что когда мы в метод getBean() не передаем тип бина, который мы хотим получить - то этот метод возвращает просто Object. а вот тут мы указываем методу getBean(), что хотим получить именно объект такого вот типа, соответственно и нет необходимости кастить объект к классу кота, так как спринг вернет именно объект кота, а не просто Object
Cat cat = context.getBean(Cat.class);
ChupaFx техник в Sber
28 сентября 2023, 18:19
Не знаю, что там у других: для меня эта статья стала первыми шагами в освоении спринга - и у меня все отлично заработало. Никаких проблем не возникло. :) У меня только один вопрос: КАК НАЙТИ СЛЕДУЮЩУЮ СТАТЬЮ??
Andrey Karelin
Уровень 41
29 мая 2022, 13:29
Материал "СУПЕР"! Ну почему мы не видим в интернете такого типа материалы (самые азы для чайников) на большинство необходимых Junior-у тем? Почему везде этот Эльфийский язык, термины, определения....старающиеся максимально точно передать смысл...вместо МАКСИМАЛЬНО ПОНЯТНО. Или может это такая обратная сторона медали....те, кто реально что-то понимает и тратит свое время на написание статей, специально это делает так же сложно, как и "изучал он"... мол, "а ха ха ХА... читайте и плачьте, ничтожества!" 😁
Rasim Valayev
Уровень 2
28 января 2022, 21:18
Четкое объяснение. Молодца.
OLGA LESOVAIA
Уровень 27
23 января 2022, 15:06
Стас, подписываюсь под всем сказанным, отлично изложили! стало понятнее, на самом деле лучшее объяснение из того что я встречала. Спасибо!