Приветствую тебя, Хабр!

Эта статья будет полезна тем, кто уже начал изучать Java и даже успел добиться некоторых успехов в понимании Java Core, и вот услышал слово Spring. И, возможно, даже не один раз: знание Spring Framework, как минимум, фигурирует в описаниях множества вакансий для джавистов. Эта статья поможет вам взобраться на самую первую ступеньку: понять общую идею столь популярного фреймворка.

Начнем издалека. Существует такое понятие как Inversion of Control, по-русски – Инверсия управления, сокращенно – IoC. IoC — один из принципов, приближающий наш код к слабосвязанности. IoC — это делегирование части наших обязанностей внешнему компоненту.

Существуют разные реализации IoC подхода, нас интересует одна из них — Dependency Injection, внедрение зависимостей. Что это такое, название говорит само за себя, так что раскрыть ее я постараюсь на примере. Мы пишем приложение, автоматизирующее работу сети магазинов. Есть классы Shop (магазин) и Seller (продавец). У класса Seller имеется поле типа Shop — магазин, в котором работает продавец. Вот мы и столкнулись с зависимостью: Seller зависит от Shop. Теперь задумаемся, как в объект Seller попадет объект Shop? Есть варианты:

  • Внедрить его через конструктор и сразу, при создании продавца, указывать магазин, в котором он работает:

public class Seller {
    
    private Shop shop;

    public Seller(Shop shop) {
        this.shop = shop;
    }
}

  • Создать сеттер и с помощью его вызова устанавливать продавцу магазин:

public class Seller {

    private Shop shop;

    public void setShop(Shop shop) {
        this.shop = shop;
    }
}

Перечисленные два способа — это реализация Dependency Injection. И, наконец, мы подобрались к спрингу: он предоставляет еще один способ внедрять зависимости.

Вообще говоря, Spring — это очень широкий набор библиотек на многие случаи жизни. Существует и Spring MVC для быстрого создания веб-приложений, и Spring Security для реализации авторизации в приложении, и Spring Data для работы с базами данных и еще куча всего. Но отдельно стоит Spring IoC — это базовый вид спринга, который реализует изучаемую нами тему — внедрение зависимостей. Spring IoC заслуживает внимания в самом начале изучения библиотек спринга по еще одной причине. Как вы увидите в процессе практической работы с другими видами спринга, для всех остальных спрингов Spring IoC используется как каркас.

Знакомство со Spring IoC начнем с главного термина: бин (англ. — bean). Самыми простыми словами,
Бин — создаваемый Spring-ом объект класса, который можно внедрить в качестве значения поля в другой объект.

Хотите словами посложнее? А пожалуйста:
Бин — объект класса, представляющий собой завершенный программный элемент с определенной бизнес-функцией либо внутренней функцией Spring'а, жизненным циклом которого управляет контейнер бинов.

Как вы уже поняли, для того, чтобы в Seller можно было внедрить Shop, Shop должен стать бином. Существует несколько способов рассказать приложению, какие объекты имеют гордое право называться бинами, все они приводят нас к понятию ApplicationContext. ApplicationContext — это сердце спринга. Как правило, он создается в самом начале работы приложения («поднимается») и управляет жизненным циклом бинов. Поэтому его еще называют контейнером бинов.

Подбираемся к главному. Каким образом нам необходимо переписать наши классы, чтобы Spring IoC и его слуга ApplicationContext подставили значение поля Shop объекту Seller? Вот таким:

@Component
public class Shop {
}

@Component
public class Seller {

    @Autowired
    private Shop shop;
}

Просто? Куда уж проще! Элегантно? Вполне. Здесь произошло следующее: аннотация Component сказала спрингу, что класс, который ей аннотируем, это бин. Аннотация Autowired попросила Spring в поле, которое она аннотирует, подставить значение. Эта операция называется «инжектнуть» (inject). Какое именно значение будет подставлено? Об этом чуть позже, сначала разберемся, как вообще классы становятся бинами.

Мы уже знаем, что в начале работы приложения должен подняться хранитель всех бинов ApplicationContext. Он-то и создает сразу все бины. Почти все. Дело в том, что по умолчанию любой бин имеет внутриспринговое свойство scope в значении singleton. Внутриспринговое, так как синглтоном в прямом смысле слова он не является. Он является синглтоном для спринга: при поднятии контекста Spring создаст ровно один объект-бин из указанного класса. Если вы хотите изменить такое поведение — пожалуйста, Spring разрешает управлять временем создания бина и их количеством для одного класса, но сейчас не об этом.

Итак, при поднятии ApplicationContext создаются все бины. Давайте выясним, а собственно где живет контекст и самое главное: как он определяет, из каких классов необходимо создавать бины. Вариантов несколько, для простоты изложения мы поговорим про один из них: конфигурирование с помощью файла xml. Вот его пример:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:beans="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="product" class="main.java.Product"></bean>
    <context:component-scan base-package="main"/>
</beans>

В этом файле продемонстрирован запуск создания бинов двумя путями. Первый, скажем так, ручной. Видите, здесь есть тег bean с указанием класса. Это и есть бин. Из всего, что прописано в этом файле с тегом bean, будут созданы бины.

Второй путь менее многословен. Помните, над классами мы поставили аннотацию Component. Из всех классов, аннотированных этой аннотацией, будут созданы бины. Благодаря этой строке из xml-файла:

<context:component-scan base-package="main"/>

Она говорит спрингу: просканируй весь пакет main и из всего, над чем будет стоять аннотация Component (или другие аннотации, являющиеся наследниками Component), создай бины. Компактно, не правда ли? Просто говорим, в каких пакетах содержатся классы, из которых нужно создавать бины, и аннотируем эти классы.

Поднять контекст с использованием xml-файла можно следующей строчкой кода:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

где beans.xml — путь к xml-нику, о котором шла речь выше.

С созданием бинов разобрались. Каким же образом Spring заполнит поле Shop при создании Seller’а? При поднятии контекста создается бин-объект класса Shop. Также создается бин-объект класса Seller, он же тоже аннотирован Component. У него есть поле типа Shop, аннотированное Autowired. Аннотация Autowired говорит спрингу: в это поле нужно инжектнуть бин. В нашем случае у нас есть всего один бин, подходящий на эту роль, то есть тип которого совпадает с типом поля: это бин — экземпляр класса Shop. Он и будет проинжектен в объект Seller, что и требовалось. Я понимаю, сейчас вопросики полезли как червячки: а что будет, если Spring не найдет нужный бин, или найдет несколько подходящих (особенно учитывая, что инжектить можно также по интерфейсу, а не по классу). Spring умен, но требует того же и от нас. Нам нужно либо иметь в системе ровно один бин, подходящий под каждый Autowired, либо обучать Spring действиям при таких конфликтах (об этом мы сейчас не будем, вы и так устали, крепитесь, статья подходит к концу).

Заметьте, что Seller – это тоже бин. Если бы он был не бином, а создавался через new, то автоматически бы ничего в него не проинжектнулось.

Возможно вы сейчас думаете, зачем все эти сложности. Но представьте, что у нас приложение не из 2 классов, а на несколько порядков больше и управление зависимостями уже становится не самой тривиальной задачей.

Возможно вы сейчас думаете, как красиво, просто и лаконично Spring позволяет внедрять зависимости. Но представьте, что что-то пошло не так и вам необходимо дебажить приложение. И все становится уже не так просто…

Парочка хинтов напоследок:

  • Если вы реализовали проект и сейчас в недоумении, как же получить бин у спринга, чтобы посмотреть на него, сделайте вот так:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Seller seller = (Seller) context.getBean(Seller.class);

Это легальный способ получить бин, хотя в современных реалиях так обычно не делают. Но для учебного примера можно.

  • Поскольку Spring — это фреймворк, необходимо подключить его в свой проект. Я создаю приложение с помощью maven и добавляю в файл pom.xml зависимости spring-core и spring-context.

Spring IoC содержит в себе огромные возможности по созданию, настройке и инжекту бинов. Мы рассмотрели крохотную часть верхушки айсберга, один из способов работы со спрингом, но я надеюсь, что нам удалось окинуть его острым взором и получить общее представление о происходящем.

Комментарии (10)


  1. shapovalex
    12.06.2019 16:45
    +3

    В принципе неплохая статья, если бы не два но:
    1. XML. Опять XML. Который устарел уже несколько лет назад и довольно сложен для понимания. К тому же дикая связь с annotation config.
    2. Инъекция в приватные поля сильно затрудняет юнит тестирование.
    Приходится применять ReflectionTestUtils и другие хаки. Лучше все-же использовать инъекцию через конструктор. Последние версии спринга прекрасно умеют инжектить в конструктор даже без аннотации Autowired на нем


    1. sshikov
      12.06.2019 21:48

      Ну, вообще-то xml может быть изменен в runtime (никто не мешает создать контекст налету, сформировав сначала кучку xml). Что до аннотаций, то это возможность времени компиляции. Так что насчет устарел я бы не был так категоричен. У них несколько разные возможности, и иногда XML может быть полезен. Да, это сильно нетипичное применение, и потенциально небезопасное к тому же.

      Сложен для понимания? Ну не знаю, мне — нет, вероятно дело привычки. Поддержка со стороны IDEA только в Ultimate версии, это да, не очень удобно. Ну то есть, если это текст для начинающих, иметь представление что XML контексты бывают — это полезно.

      Мне вот другое любопытно. Спрингу сто лет в обед. Autowired там появились кажется примерно в тоже время, что и JavaEE 6, т.е. лет 10 назад. Ресурсов на эту тему в интернете куча, в том числе на русском. Эта тема тут реально кому-то интересна?


      1. oponomarev
        13.06.2019 12:30

        Можете привести пример из практики, когда в продакшене приходилось пересоздать контекст, не модифицируя при этом код?


        1. sshikov
          13.06.2019 18:46

          В контексте много чего бывает, кроме бинов, которые пожалуй единственные завязаны на код. Данные, коннекты к базам и JMS, как самые очевидные примеры. В моей практике — camel routes, много раз. И не только пересоздать, а и создать новый, еще один.


      1. serginio89
        14.06.2019 06:48

        Часто приходится на лету в рантайме менять xml контекст?


        1. sshikov
          14.06.2019 18:55

          Зависит от проекта. Причем от версии спринга скорее не зависит. У меня опыта с этим фреймворком с 2005 года примерно, и тогда просто не было никаких других контекстов, кроме XML — но желания загрузить контекст в рантайме не возникало, хотя я и тогда знал, как это делается. Не было потребности.

          А были проекты, где примерно треть контекстов вот так вот грузилась время от времени.

          Опять же — я не призываю так делать, были вполне живые эксплойты на такую тему, вполне очевидно, что это позволяет выполнять фактически произвольный код, если разработчик был неаккуратен. Я просто ради того, чтобы понимать, где средства компиляции, а где рантайм.


    1. Raspy
      13.06.2019 08:44

      1. Можно без xml. Вместо ClassPathXmlApplicationContext берёте AnnotationConfigApplicationContext и в качестве аргумента передаёте в него класс, помеченный аннотацией @Configuration
      2. Совершенно наоборот. При тестировании вы легко используете тестовый апликейшен контекст, в котором могут быть реализованы мокированные объекты, необходимые для тестов и инъекция будет абсолютно прозрачной.


      1. pomazkina Автор
        13.06.2019 08:54

        В черновой версии статьи я упоминала и про AnnotationConfigApplicationContext, но потом решила, что не стоит превращать статью в простыню и про виды контекста рассказать в следующий раз, если будет спрос.


  1. Throwable
    13.06.2019 18:55

    Создать сеттер и с помощью его вызова устанавливать продавцу магазин:

    Это заведомо плохое решение, т.к. сразу позволяет создавать "ненастроенные" объекты как продавец без магазина. Сеттеры еще можно использовать для опциональных полей, но для обязательных — т.е. тех, при которых объект не будет работать — лучше использовать конструктор и final-поля. Именно он, собственно, и задают dependencies.


    И, наконец, мы подобрались к спрингу: он предоставляет еще один способ внедрять зависимости.

    А это самый быстрый и самый плохой способ. Все ваши объекты становятся въявную завязанными на контейнер и без него уже не способны работать. Ни протестировать нормально ни переиспользовать потом.


    Возможно вы сейчас думаете, зачем все эти сложности. Но представьте, что у нас приложение не из 2 классов, а на несколько порядков больше и управление зависимостями уже становится не самой тривиальной задачей.

    Уж поверьте, еще более нетривиальной задачей это становится со спрингом. Особенно когда в проекте несколько разрабов. Когда все внеявную завязано на текущих настройках и умолчаниях, любое изменение либо не тупо работает, либо что-то рушит. Поэтому типичный проект на спринге связывает не более десятка бинов, и используют его совсем не из-за DI, а из-за легкого бутстрапа различных технологий из коробки.


    Если нужен DI, возьмите Guice.


  1. manulus
    14.06.2019 06:49

    Эта статья поможет вам взобраться на самую первую ступеньку: понять общую идею столь популярного фреймворка.

    Из статьи не понял ни что такое Spring ни для чего он нужен и как помогают эти зависимости.