«1. Обзор

В этом руководстве мы узнаем, как использовать аннотацию Spring @Import, а также объясним, чем она отличается от @ComponentScan.

2. Конфигурация и бины

Прежде чем понять аннотацию @Import, нам нужно знать, что такое бин Spring, и иметь базовые практические знания аннотации @Configuration.

Обе темы выходят за рамки данного руководства. Тем не менее, мы можем узнать о них в нашей статье о Spring Bean и в документации Spring.

Предположим, что мы уже подготовили три bean-компонента — Bird, Cat и Dog — каждый со своим классом конфигурации.

Затем мы можем обеспечить наш контекст этими классами Config:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { BirdConfig.class, CatConfig.class, DogConfig.class })
class ConfigUnitTest {

    @Autowired
    ApplicationContext context;

    @Test
    void givenImportedBeans_whenGettingEach_shallFindIt() {
        assertThatBeanExists("dog", Dog.class);
        assertThatBeanExists("cat", Cat.class);
        assertThatBeanExists("bird", Bird.class);
    }

    private void assertThatBeanExists(String beanName, Class<?> beanClass) {
        Assertions.assertTrue(context.containsBean(beanName));
        Assertions.assertNotNull(context.getBean(beanClass));
    }
}

3. Группировка конфигураций с помощью @Import

Нет проблем с объявлением всех конфигураций. Но представьте себе проблему управления десятками классов конфигурации в разных источниках. Должен быть лучший способ.

У аннотации @Import есть решение, позволяющее группировать классы конфигурации:

@Configuration
@Import({ DogConfig.class, CatConfig.class })
class MammalConfiguration {
}

Теперь нам просто нужно вспомнить млекопитающих:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { MammalConfiguration.class })
class ConfigUnitTest {

    @Autowired
    ApplicationContext context;

    @Test
    void givenImportedBeans_whenGettingEach_shallFindOnlyTheImportedBeans() {
        assertThatBeanExists("dog", Dog.class);
        assertThatBeanExists("cat", Cat.class);

        Assertions.assertFalse(context.containsBean("bird"));
    }

    private void assertThatBeanExists(String beanName, Class<?> beanClass) {
        Assertions.assertTrue(context.containsBean(beanName));
        Assertions.assertNotNull(context.getBean(beanClass));
    }
}

Ну, наверное, мы забудем нашу Птицу скоро, так что давайте сделаем еще одну группу, чтобы включить все классы конфигурации животных:

@Configuration
@Import({ MammalConfiguration.class, BirdConfig.class })
class AnimalConfiguration {
}

Наконец, никто не остался позади, и нам просто нужно запомнить один класс:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { AnimalConfiguration.class })
class AnimalConfigUnitTest {
    // same test validating that all beans are available in the context
}

4. @Import vs @ComponentScan

Прежде чем перейти к примерам @Import, давайте сделаем быструю остановку и сравним его с @ComponentScan.

4.1. Сходства

Обе аннотации могут принимать любой класс @Component или @Configuration.

Давайте добавим новый @Component с помощью @Import:

@Configuration
@Import(Bug.class)
class BugConfig {
}

@Component(value = "bug")
class Bug {
}

Теперь bean-компонент Bug доступен так же, как и любой другой bean-компонент.

4.2. Концептуальная разница

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

Чтобы ответить на этот вопрос, давайте вспомним, что Spring обычно продвигает подход, основанный на соглашениях, а не на конфигурации.

Проводя аналогию с нашими аннотациями, @ComponentScan больше похож на соглашение, а @Import — на конфигурацию.

4.3. Что происходит в реальных приложениях

Обычно мы запускаем наши приложения, используя @ComponentScan в корневом пакете, чтобы он мог найти все компоненты для нас. Если мы используем Spring Boot, то @SpringBootApplication уже включает @ComponentScan, и все готово. Это показывает силу условностей.

Теперь давайте представим, что наше приложение сильно растет. Теперь нам нужно иметь дело с bean-компонентами из разных мест, такими как компоненты, различные структуры пакетов и модули, созданные нами или третьими сторонами.

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

С другой стороны, мы не хотим писать @Import для каждого нового компонента, потому что это контрпродуктивно.

Возьмем, к примеру, наших животных. Мы действительно могли бы скрыть импорт из объявления контекста, но нам все равно нужно помнить @Import для каждого класса Config.

4.4. Работая вместе

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

Тогда у нас может быть один @ComponentScan только для нашего пакета животных:

package com.baeldung.importannotation.animal;

// imports...

@Configuration
@ComponentScan
public class AnimalScanConfiguration {
}

И @Import, чтобы сохранить контроль над тем, что мы добавим в контекст:

package com.baeldung.importannotation.zoo;

// imports...

@Configuration
@Import(AnimalScanConfiguration.class)
class ZooApplication {
}

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

5. Заключение

В этом кратком руководстве мы узнали, как использовать @Import для организации наших конфигураций.

Мы также узнали, что @Import очень похож на @ComponentScan, за исключением того факта, что @Import имеет явный подход, а @ComponentScan использует неявный.

Кроме того, мы рассмотрели возможные трудности с управлением нашими конфигурациями в реальных приложениях и способы их решения путем объединения обеих аннотаций.

Как обычно, полный код доступен на GitHub.