Dependency Injection (внедрение зависимостей) – ключевой шаблон проектирования в Spring. Мы говорим фреймворку создать за нас бины (иначе говоря — объекты) и внедрить их в другие бины. И фреймворк это делает.
Но как объяснить фреймворку Spring, что такой-то бин должен стать зависимостью для другого бина? Вариантов немного, а самых частых всего два: бин внедряется либо через конструктор класса, либо с помощью сеттера. Первое называется constructor-based injection, а второе — setter-based injection.
В этой статье мы создадим бин Engine и будем внедрять его в два других бина: в бин CarWithConstructor с помощью конструктора и в CarWithSetter с помощью сеттера.
Конфигурация Maven
Чтобы начать работу с бинами, необходимо добавить в pom.xml зависимость:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.4.RELEASE</version> </dependency>
Определим классы. Итак, сначала у нас есть три класса. Класс Engine:
public class Engine { }
Класс CarWithConstructor с конструктором:
public class CarWithConstructor { private Engine engine; public CarWithConstructor(Engine engine) { this.engine = engine; } public Engine getEngine() { return engine; } }
И класс CarWithSetter с сеттером:
public class CarWithSetter { private Engine engine; public void setEngine(Engine engine) { this.engine = engine; } public Engine getEngine() { return engine; } }
Чтобы внедрить бин, классов нам недостаточно, Spring имеет дело с бинами, а не классами. Поэтому нужно сконфигурировать эти классы так, чтобы Spring контейнер создал на их основе бины. В конфигурации заодно будут заданы и pзависимости. Конфигурировать бины можно либо с помощью аннотаций, либо с помощью XML. (Но учтите, что XML-конфигурация немного устарела.)
Конфигурация бинов с помощью аннотаций
До того как внедрять бин engine, давайте его определим:
@Component public class Engine { }
Аннотация @Component говорит фреймворку превратить класс в бин. При запуске Spring создаст экземпляр класса Engine. Этот экземпляр будет синглтоном в нашем случае. Мы сможем его впоследствии получить из контекста приложения с помощью команды:
context.getBean(Engine.class);
И он будет внедрен во все бины, где мы зададим его в качестве зависимости. Неважно каким способом – через конструктор или сеттер.
Давайте зададим пакет, в котором хранятся бины, чтобы Spring знал, где их искать. Это делается с помощью аннотации @ComponentScan:
@ComponentScan("ru.javalang.injection") public class Config{ }
Обычно в классе Config прописываются конфигурации, но в нашем простом приложении он пуст.
В пакете «ru.javalang.injection» Spring будет искать аннотированные с помощью @Component классы, чтобы превратить их в бины при запуске приложения и инициализации контейнера Spring.
Итак, мы определили один бин engine. Теперь можно его внедрять в другие бины. Конечно, эти другие бины тоже надо сконфигурировать. И внутри конфигурации задать зависимости (dependency injection).
Constructor Based Injection
Если в классе есть конструктор, то можно внедрить зависимость через конструктор. При создании класса контейнер Spring вызовет конструктор и передаст зависимость в качестве аргумента конструктора.
Давайте определим бин CarWithConstructor и внедрим в него бин Engine с помощью конструктора:
@Component public class CarWithConstructor { private Engine engine; @Autowired public CarWithConstructor(Engine engine) { this.engine = engine; } public String toString() { return "car" + " with " + engine; } }
Аннотация @Component означает, что класс CarWithConstructor надо зарегистрировать в качестве бина.
А аннотация @Autowired перед конструктором говорит фреймворку внедрить бин engine в качестве зависимости в бин CarWithConstructor.
Обратите внимание, что начиная с версии Spring 4.3 аннотацию @Autowired можно опустить, если у класса всего один конструктор. О том, что в конструкторе надо внедрить бин, фреймворк догадается сам.
Setter Based Injection
Если в классе задан сеттер, то зависимость можно внедрить и через него. Тогда при создании экземпляра класса контейнер вызовет конструктор без аргументов, а потом сеттер, чтобы внедрить зависимость во только что созданный бин.
Определим бин CarWithSetter и внедрим в него бин engine с помощью сеттера.
Для этого используем перед сеттером аннотацию @Autowired:
@Component public class CarWithSetter { private Engine engine; @Autowired public void setEngine(Engine engine) { this.engine = engine; } public String toString() { return "car" + " with " + engine; } }
Так же, как в предыдущем случае, аннотацию @Autowired перед сеттером можно опустить.
Более того, можно опустить и сеттер. И просто аннотировать поле engine:
@Component public class CarWithSetter{ @Autowired private Engine engine; public Engine getEngine() { return engine; } }
И внедрение зависимости все равно произойдет. Несмотря на то, что тут нет ни конструктора, ни сеттера, а поле engine имеет модификатор private. Это возможно, потому что под капотом фреймворк использует рефлексию для создания бинов. (Но такой вариант (field-based dependency injection) не рекомендуется).
Чтобы получить экземпляры машин, надо обратиться к контексту приложения:
@Test public void givenAnnotationConfig_whenConstructorInjected_ThenEngineExist() { ApplicationContext javaConfigContext = new AnnotationConfigApplicationContext(Config.class); CarWithConstructor carWithConstructor = javaConfigContext.getBean(CarWithConstructor.class); assertNotNull(carWithConstructor.getEngine()); } @Test public void givenAnnotationConfig_whenSetterInjected_ThenEngineExist() { ApplicationContext javaConfigContext = new AnnotationConfigApplicationContext(Config.class); CarWithSetter carWithSetter = javaConfigContext.getBean(CarWithSetter.class); assertNotNull(carWithSetter.getEngine()); }
Переменная carWithConstructor будет иметь ненулевую ссылку на engine. Хотя мы не создавали ни один объект с помощью оператора new. Все бины создал фреймворк и добавил ссылки на зависимости там, где они были определены.
Обратите внимание, что все бины у нас синглтоны, и обе переменные carWithConstructor и carWithSetter ссылаются на один и тот же engine. Синглтон — самый частый жизненный цикл бина.
Конфигурация бинов с XML
А теперь сконфигурируем все то же самое с помощью XML:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="engine" class="ru.javalang.injection.Engine" /> <bean class="ru.javalang.injection.CarWithConstructor"> <constructor-arg ref="engine" /> </bean> <bean class="ru.javalang.injection.CarWithSetter"> <property name="engine" ref="engine" /> </bean> </beans>
Тег bean задает бин, это аналог аннотации @Component.
Constructor Based Injection
Вот часть вышеприведенного XML, которая определяет бин CarWithConstructor:
<bean class="ru.javalang.injection.CarWithConstructor"> <constructor-arg ref="engine" /> </bean>
Тут constructor-arg определяет внедрение зависимости с помощью конструктора.
Атрибут ref содержит ссылку на идентификатор бина engine.
Setter-Based Injection
А это часть вышеприведенного XML, которая задает бин CarWithSetter:
<bean class="ru.javalang.injection.CarWithSetter"> <property name="engine" ref="engine" /> </bean>
Здесь тег property задает внедрение зависимости с помощью сеттера.
Обратите внимание, что если мы конфигурируем бины с помощью XML, то задать сеттер в классе необходимо. Иначе будет выброшено исключение. Потому что все послабления в конфигурациях пришли с аннотациями, с XML все гораздо строже.
За контекст, созданный с помощью XML, отвечает другой класс:
ApplicationContext xmlConfigContext = new ClassPathXmlApplicationContext("injection.xml");
Бин из XML-контекста получаем аналогично:
CarWithConstructor carWithConstructor = xmlConfigContext .getBean(CarWithConstructor.class);
Убедимся, что engine внедрен:
@Test public void givenXmlConfig_whenConstructorInjected_ThenEngineExist() { ApplicationContext xmlConfigContext = new ClassPathXmlApplicationContext("injection.xml"); CarWithConstructor carWithConstructor = xmlConfigContext.getBean(CarWithConstructor.class); assertNotNull(carWithConstructor.getEngine()); } @Test public void givenXmlConfig_whenSetterInjected_ThenEngineExist() { ApplicationContext xmlConfigContext = new ClassPathXmlApplicationContext("injection.xml"); CarWithSetter carWithSetter = xmlConfigContext.getBean(CarWithSetter.class); assertNotNull(carWithSetter.getEngine()); }
Какой способ внедрения зависимости лучше
Для разработчика большой разницы нет. В документации рекомендуется отталкиваться от класса – его структуры и цели. Если зависимость обязательна в данном классе, то логичнее это поле передавать в конструкторе. А значит это будет внедрение через конструктор. Соответственно если какая-то зависимость необязательна, то внедряем ее через сеттер.
Код примера есть на GitHub.
Большое спасибо вам за ваши статьи!!! Все очень коротко, ястно и лаконично объясняется…Однозначно лучше чем любой видео-урок на ютубе.
Спасибо, будем продолжать). Правда, как раз хочу записать краткие видео-уроки.
Огромная благодарность Автору статей по Spring, это самые ясные и лаконичные разъяснения работы фреймворка, которые я нашел!
Искренне Ваш, Yustas
«Более того, можно опустить и сеттер. И просто аннотировать поле car:» Кажется здесь ошибка, имелся ввиду не car а engine? И пример кода не тот.
да, что-то тут напутано, спасибо за внимательность. Исправлено.
Топ статьи, быстро и понятно.
Вопрос, скорее всего, тупой. Скачал с гитхаба ваши файлы. А как их запустить? Там же все файлы public…
Открыть в IntelliJ IDEA папку spring-core — этот проект сразу к нескольким статьям относится, в т.ч. к этой. Далее запускать не main метод, а тесты в классе InjectionTest.
Либо с нуля самостоятельно проект создать, можно тут https://start.spring.io/.
Понял. Спасибо) Ваши курсы по spring лучшие в российском интернете.