Способы внедрения зависимостей (Dependency Injection) в Spring

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 {
    @Autowired
    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public String toString() {
        return "car" + " with " + engine;
    }
}

Так же как в предыдущем случае, аннотацию @Autowired перед сеттером можно опустить.
Более того, можно опустить и сеттер. И просто аннотировать поле car:

@Component
public class CarWithConstructor {
    private Engine engine;

    @Autowired
    public CarWithConstructor(Engine engine) {
        this.engine = engine;
    }

    public Engine getEngine() {
        return engine;
    }
}

И внедрение зависимости все равно произойдет. Несмотря на то, что тут нет ни конструктора, ни сеттера, а поле car имеет модификатор private. Это возможно, потому что под капотом фреймворк использует рефлексию для создания бинов.

Чтобы получить экземпляры машин, надо обратиться к контексту приложения:

@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.

Способы внедрения зависимостей (Dependency Injection) в Spring: 2 комментария

  1. Большое спасибо вам за ваши статьи!!! Все очень коротко, ястно и лаконично объясняется…Однозначно лучше чем любой видео-урок на ютубе.

    1. Спасибо, будем продолжать). Правда, как раз хочу записать краткие видео-уроки.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *