Пользователь Стас Пасинков
Стас Пасинков
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 и сделаем простенькое веб-приложение, используя спринг.
Комментарии (79)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
7 декабря 2020
Классно и понятно объяснили. Спасибо большое. Когда будет 3я часть?
Bli 17 уровень, Минск
5 декабря 2020
Очень понятное обьяснение!
Interstellar 36 уровень, Воронеж Expert
16 октября 2020
Очень доходчиво написано. Вам можно было бы продолжать писать про Спринг серию статей. Скопы бинов с примерами, контроллеры, авторизация, подключение БД и прочее.
Chundrik 35 уровень, Санкт-Петербург
25 сентября 2020
Посмеялась с ru.javarush.info.fatfaggy.animals.entities А статья просто шикарная, спасибо!
Роман 24 уровень
24 июля 2020
Ты лучший так объяснять доно лишь БОГУ. БРАВО ХОЧУ ЕЩЁ ТВОИХ СТАТЕЙ
Ильнур 17 уровень
10 июля 2020
Пиши ещё, это божественное объяснение
Павел 41 уровень, Санкт-Петербург
5 июля 2020
Мои проекты на Spring. Тут можно посмотреть как все это работает в комплексе. Плюс есть комментарии в коде. https://github.com/Novoselov-pavel/small-spring-log https://github.com/Novoselov-pavel/small-site-spring-log
Vasily Rudenkov 1 уровень, Москва
1 июля 2020
Мне ради того, чтобы оставить коммент пришлось авторизоваться))) (пароль вспоминал 5 мин). Спасибо огромное за статью! Никакого лишнего говна, а четко по теме!
Артём Баранов 24 уровень
19 июня 2020
Большая просьба к автору - пиши ещё! ) Спасибо за статью
Yury 0 уровень
12 апреля 2020
Просто как 2х2. Огромное спасибо автору! репозиторий с кодом по данной статье