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 марта 2024
В следующей статье мы резко нырнем в spring-web-mvc и сделаем простенькое веб-приложение, используя спринг. Подскажите где можно ознакомиться со следующей статьей?
Владислав Уровень 82 Expert
20 января 2024
Week week = context.getBean(Week.class); System.out.println("It s " + week.getWeekDayName()); объясните откуда метод getWeekDayName знает какой день недели. Если Всю логику выполняет метод конфига
Владислав Уровень 82 Expert
19 января 2024
хоть я сейчас только на половине статьи, но у вас шикарная подача материала. Спасибо за ваши труды и хочу видеть от вас больше полезной информации на сложноватые для новичков темы.
Alexey Уровень 4
14 ноября 2022
что-то скриншот из идеи совсем плох
Владимир Уровень 1
20 июля 2022
Понял! Надо из MyConfig убрать объявление просто кота.
Владимир Уровень 1
20 июля 2022
Я не понял, как создавать кота-убийцу. Т. е. как коту передать попугая?
Nik Уровень 44
15 июля 2022
Может это я такой тупой, но, вместо того, чтобы за полчаса ознакомиться с материалом, мне надо пол дня разбираться, что недосказано и недописано, чтобы эти примеры заработали! И так у всех авторов!
Andrey Karelin Уровень 41
29 мая 2022
Материал "СУПЕР"! Ну почему мы не видим в интернете такого типа материалы (самые азы для чайников) на большинство необходимых Junior-у тем? Почему везде этот Эльфийский язык, термины, определения....старающиеся максимально точно передать смысл...вместо МАКСИМАЛЬНО ПОНЯТНО. Или может это такая обратная сторона медали....те, кто реально что-то понимает и тратит свое время на написание статей, специально это делает так же сложно, как и "изучал он"... мол, "а ха ха ХА... читайте и плачьте, ничтожества!" 😁
Rasim Valayev Уровень 2
28 января 2022
Четкое объяснение. Молодца.
OLGA LESOVAIA Уровень 27
23 января 2022
Стас, подписываюсь под всем сказанным, отлично изложили! стало понятнее, на самом деле лучшее объяснение из того что я встречала. Спасибо!