JavaRush /Курсы /Spring Core /Смешанная конфигурация без хаоса

Смешанная конфигурация без хаоса

Spring Core
23 уровень , 3 лекция
Открыта

1. Смешанная конфигурация как норма

Когда XML уже перестал казаться отдельной технологией, возникает более приземлённый вопрос: как жить с ним внутри нормального приложения, где уже есть Java config. Когда мы слышим «смешанная конфигурация», мозг новичка обычно рисует хаос: половина проекта в XML, половина в аннотациях, и где-то между ними прячется баг, который появляется только по вторникам. На практике всё гораздо спокойнее, если заранее принять простую мысль: миграция — это процесс, а не событие. И в процессе вам нужно уметь держать два синтаксиса в одном приложении так, чтобы контейнер оставался предсказуемым.

Представьте типичный сценарий. У вас есть проект, который уже перевели на @Configuration + @ComponentScan, но в нём остался небольшой legacy-модуль (например, уведомления), который трогать страшно: «работает — не трогай». При этом вам нужно продолжать развивать проект рядом: добавлять новые бины, менять wiring, подключать новые реализации интерфейсов. Полный «big bang» перенос всего XML в Java config часто слишком рискованный и дорогой — особенно если вы не уверены, что понимаете каждую строчку legacy-файла.

Смешанная конфигурация — это и есть инженерный компромисс: мы подключаем XML к современному контексту, делаем проект запускаемым, а потом переносим по кусочкам, пока не останется только Java config. Главное — делать это контролируемо, а не «ну давайте импортнём всё и посмотрим, что взорвётся».

2. Legacy XML: отдельный контекст или общий контейнер

Когда у вас есть XML-файл, самый простой «технический» способ — поднять его отдельным ClassPathXmlApplicationContext и достать из него пару бинов. Это реально работает и иногда полезно для экспериментов. Но если ваша цель — реальное приложение (наш ContextFlow), то почти всегда хочется, чтобы Java config и XML жили в одном ApplicationContext, иначе начинается весёлый квест «как передать бин из одного контейнера в другой».

Давайте сравним два подхода в таблице, без религиозных войн и без «XML — зло, Java — добро» (оба синтаксиса умеют одинаково хорошо описывать BeanDefinition):

Подход Как выглядит Плюсы Минусы
Отдельный XML-контекст new ClassPathXmlApplicationContext("...xml") Быстро проверить legacy-фрагмент, изолировать эксперимент Бины не живут в одном контейнере, wiring между мирами становится неудобным
Общий контекст через импорт @ImportResource("classpath:...xml") внутри Java config Один контейнер, единое разрешение зависимостей, проще мигрировать по шагам Нужно следить за конфликтами имён и дублирующими definitions

В ContextFlow нам нужен именно второй вариант: один контейнер, один граф зависимостей, один набор профилей/ресурсов/слушателей событий — и маленький legacy-островок, который мы постепенно «цивилизуем».

Вот схема, как это мысленно выглядит (да, это та же bean model, просто источников определения больше):

flowchart TD
  Java["@Configuration / @Bean / scanning"] --> BD[BeanDefinitions registry]
  XML["legacy *.xml"] --> BD
  BD --> Beans["Созданные bean instances"]
  Beans --> App["ContextFlow: сценарии, сервисы, listeners"]

Ключевой момент: @ImportResource не создаёт «второй контейнер». Он просто добавляет ещё один источник BeanDefinition в текущий контейнер.

3. @ImportResource: что это такое и что он делает на самом деле

@ImportResource — это аннотация из мира Java-конфигурации, которая говорит Spring: «вот тебе XML-ресурс, прочитай оттуда bean definitions и добавь их в этот же контекст». Никакой отдельной магии: контейнер по-прежнему строит единый registry бинов, а потом создаёт объекты, внедряет зависимости и применяет lifecycle.

Минимальный пример bridge-конфигурации (как правило, у нас это пакет config.legacybridge):

package com.example.contextflow.config.legacybridge;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
// Подключаем legacy XML в ТОТ ЖЕ ApplicationContext (не создаём отдельный контейнер)
@ImportResource("classpath:/legacy/legacy-notification-context.xml")
public class LegacyBridgeConfig {
    // Класс пустой — он нужен как «якорь» для импорта XML-ресурса
}

Обратите внимание на classpath:/.... Это важно по двум причинам. Во‑первых, XML лежит в src/main/resources, значит на runtime он будет доступен в classpath (внутри jar — тоже). Во‑вторых, мы уже учили Resource abstraction: «classpath» — это не «файл на диске», это ресурс приложения.

Даже если этот XML потом вырастет ещё парой bean-ов, точка входа не меняется: bridge-конфиг всё так же просто подмешивает его definitions в общий контейнер.

Дальше мы собираем основной контекст, как обычно, и просто добавляем LegacyBridgeConfig в список конфигураций:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

// Важно: один AnnotationConfigApplicationContext, внутри которого будут и @Bean, и XML-bean definitions
try (var ctx = new AnnotationConfigApplicationContext(
        com.example.contextflow.config.core.CoreConfig.class,
        com.example.contextflow.config.legacybridge.LegacyBridgeConfig.class
)) {
    // Проверяем, что бин из XML реально зарегистрировался в общем контексте
    System.out.println(ctx.containsBean("legacyNotificationSender")); // true
}

Если вы видите true, это означает, что legacyNotificationSender зарегистрирован как bean definition и доступен в контексте. Важно: мы не полезли за ним через отдельный ClassPathXmlApplicationContext. Мы не создали второй контейнер. Мы просто добавили XML-определения в общий.

Ещё один практический нюанс. @ImportResource принимает массив строк, так что можно подключать несколько маленьких XML-фрагментов (но именно маленьких, иначе вы снова получаете «гигантский XML-монолит», только теперь он импортирован):

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
@ImportResource({
        // Можно импортировать несколько XML-фрагментов, если держите legacy по модулям
        "classpath:/legacy/legacy-notification-context.xml",
        "classpath:/legacy/legacy-audit-context.xml"
})
class LegacyBridgeConfig {
}

В нашем курсе мы держим один legacy fragment, так что это скорее иллюстрация, чем рекомендация «подключите все ваши 30 XML».

4. Wiring между Java config и XML: зависимости в обе стороны

Самое полезное в @ImportResource — то, что бины из XML и бины из Java config начинают жить как соседи по лестничной клетке: знают друг о друге, могут ссылаться друг на друга и даже спорить за место под солнцем. Это удобно, но именно тут важно не потерять голову: один контейнер означает и единый механизм разрешения кандидатов.

На одном фрагменте это видно лучше всего. В XML живёт legacyNotificationSender, и он уже использует инфраструктурный бин messageSource из общего контекста:

<bean id="legacyNotificationSender"
      class="com.example.contextflow.infrastructure.notification.legacy.LegacyConsoleNotificationSender"
      init-method="init" destroy-method="destroy">
    <property name="appName" value="ContextFlow (legacy)"/>
    <property name="messageSource" ref="messageSource"/>
</bean>

А в Java config мы создаём сервис, который хочет использовать именно эту legacy-реализацию:

import com.example.contextflow.application.service.NotificationDispatchService;
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class NotificationModuleConfig {

    @Bean
    NotificationDispatchService notificationDispatchService(
            // Берём именно legacy-бин из XML по его id
            @Qualifier("legacyNotificationSender") NotificationSender sender
    ) {
        return new NotificationDispatchService(sender);
    }
}

Здесь в одной картинке видно обе стороны связи. XML-бин получает messageSource из общего контекста через ref="messageSource". Java config, в свою очередь, выбирает legacyNotificationSender по qualifier и строит на нём сервис. Контейнер один, candidate resolution один, lifecycle один.

Если NotificationSender был бы единственным кандидатом, qualifier мог бы и не понадобиться. Но в ContextFlow реализаций несколько, поэтому явное имя здесь спокойнее: сразу видно, что сервис сидит именно на legacy-ветке.

5. Пошаговая миграция XML → Java config

Самая частая ошибка новичка — попытаться «просто переписать XML на Java» за вечер. Это примерно как «переписать весь проект на microservices» за выходные: теоретически возможно, но обычно это заканчивается тем, что у вас есть два проекта — старый и “почти новый”, и ни один не запускается. Пошаговая миграция нужна, чтобы в каждый момент времени приложение оставалось в рабочем состоянии.

Ниже — стратегия, которую удобно держать как план и проговаривать вслух, когда вы смотрите на legacy XML и чувствуете лёгкое желание уехать в лес без интернета.

Шаг A. Изолируем legacy-фрагмент и подключаем через @ImportResource

Первый шаг — вообще признать, что это legacy. В ContextFlow мы держим такие файлы в src/main/resources/legacy/ и подключаем их через отдельный конфиг, например LegacyBridgeConfig. Это даёт вам визуальную границу: «вот тут legacy». Психологически это уже облегчает жизнь, потому что XML перестаёт расползаться по проекту как конфетти.

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
@ImportResource("classpath:/legacy/legacy-notification-context.xml")
class LegacyBridgeConfig {
}

Важно: на этом шаге мы ничего не мигрируем. Мы просто делаем проект запускаемым и контролируемым.

Шаг B. Делаем «карту» XML: id → class → зависимости → lifecycle

Теперь нужен маленький инвентаризационный ритуал. Не страшный, просто инженерный. Мы выписываем каждый <bean> и фиксируем, что он делает. Это можно держать даже в виде таблички в заметках или прямо в комментарии.

Пример карты для notification-фрагмента, где уже есть sender и завязанный на него сервис:

bean id class injection depends on lifecycle
legacyNotificationSender LegacyConsole NotificationSender property messageSource (ref), appName (value) init, destroy
legacyNotification DispatchService NotificationDispatchService constructor legacyNotificationSender (ref)

Когда у вас есть карта, вы перестаёте «угадывать, что хотел сказать автор XML», и начинаете видеть реальную модель контейнера.

Шаг C. Выбираем один бин для миграции и переносим его в Java config

Хороший первый кандидат — локальный бин, который не тащит за собой полпроекта. В нашем случае это как раз legacyNotificationSender: у него уже есть и свойства, и ссылка на другой bean, и lifecycle, но граф вокруг него всё ещё маленький и понятный.

XML:

<bean id="legacyNotificationSender"
      class="com.example.contextflow.infrastructure.notification.legacy.LegacyConsoleNotificationSender"
      init-method="init" destroy-method="destroy">
    <property name="appName" value="ContextFlow (legacy)"/>
    <property name="messageSource" ref="messageSource"/>
</bean>

Java-эквивалент:

import com.example.contextflow.infrastructure.notification.legacy.LegacyConsoleNotificationSender;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class NotificationConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    LegacyConsoleNotificationSender legacyNotificationSender(MessageSource messageSource) {
        var sender = new LegacyConsoleNotificationSender();
        sender.setAppName("ContextFlow (legacy)");
        sender.setMessageSource(messageSource);
        return sender;
    }
}

Мы сознательно сохранили имя legacyNotificationSender: именно его уже знают XML-ref, @Qualifier и диагностический код.

Шаг D. Удаляем/отключаем старое объявление из XML и снова запускаем приложение

Это критичный момент дисциплины. Пока бин объявлен и в XML, и в Java config, контейнер либо перезапишет определение (если overriding разрешён), либо упадёт (если запрещён). И в обоих случаях вы получаете риск тихой поломки: «кажется, мы мигрировали, но на самом деле работает старая версия».

Поэтому правило: перенёс бин — убрал из XML. Маленькими шагами, но честно.

Шаг E. Повторяем для следующего бина

Очень важный психологический пункт. Миграция — это не рефакторинг архитектуры. В момент переноса у вас будет соблазн: «а давайте заодно заменим setter injection на constructor injection», «а давайте заодно переделаем интерфейс», «а давайте заодно выкинем половину бинов». Не надо. Это потом. Сейчас цель — сохранить поведение, но поменять синтаксис конфигурации.

Если XML делал setter injection, вы мигрируете его как setter injection в @Bean-методе, даже если вы очень любите конструкторы. Конструкторы мы любим, но production-миграция любит стабильность.

6. Перенос зависимостей и lifecycle

Когда миграция доходит до бинов посложнее, важно научиться переводить XML-формы wiring в Java-формы, не теряя поведения. Хорошая новость: вы уже знаете все идеи (конструктор, свойства, lifecycle). Плохая новость: в legacy-файлах это может быть намешано.

Миграция constructor-arg ref="..." в Java config

XML:

<bean id="auditService" class="com.example.contextflow.application.service.AuditService">
    <constructor-arg ref="auditWriter"/>
</bean>

Java config (самый прямой аналог — параметр @Bean-метода):

import com.example.contextflow.application.service.AuditService;
import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class AuditConfig {

    @Bean
    AuditService auditService(AuditWriter auditWriter) {
        return new AuditService(auditWriter);
    }
}

Spring сам подставит AuditWriter из контекста. Если кандидатов несколько — используете @Qualifier, как вы уже умеете.

Миграция <property name="..."> в Java config

XML:

<bean id="reportOutputManager"
      class="com.example.contextflow.support.lifecycle.ReportOutputManager">
    <property name="outputDir" value="build/reports"/>
</bean>

Java config (мы создаём объект и вызываем setter):

import com.example.contextflow.support.lifecycle.ReportOutputManager;
import java.nio.file.Path;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class ReportingConfig {

    @Bean
    ReportOutputManager reportOutputManager() {
        var m = new ReportOutputManager();
        m.setOutputDir(Path.of("build/reports"));
        return m;
    }
}

Да, это выглядит «не так красиво», как конструктор. Но это честная миграция: в XML был property injection — в Java config он тоже есть. И здесь важно не пропустить ещё один момент: в XML строковое value="build/reports" контейнер дополнительно конвертировал в нужный тип. В Java config целевой тип обычно создаётся явно, поэтому здесь появляется Path.of(...).

Миграция init-method / destroy-method в Java config

XML:

<bean id="templateCatalog"
      class="com.example.contextflow.infrastructure.resources.NotificationTemplateCatalog"
      init-method="loadTemplates"
      destroy-method="clearCache"/>

Java config:

import com.example.contextflow.infrastructure.resources.NotificationTemplateCatalog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class TemplatesConfig {

    // Прямой перенос init-method/destroy-method из XML в @Bean-метаданные
    @Bean(initMethod = "loadTemplates", destroyMethod = "clearCache")
    NotificationTemplateCatalog templateCatalog() {
        return new NotificationTemplateCatalog();
    }
}

Это прямой перевод lifecycle-настройки в другой синтаксис. И он хорошо ложится на то, что мы изучали в дне про lifecycle: @Bean(initMethod=...) — нормальный инструмент, особенно при миграции, когда вы не хотите лезть в код класса и добавлять @PostConstruct.

7. Контроль границ: конфликты имён, дубли и scanning из XML

Смешанная конфигурация ломается обычно не потому, что @ImportResource «плохой», а потому что мы перестаём контролировать границы. В legacy XML легко встретить context:component-scan, context:annotation-config, property placeholder инфраструктуру и даже какие-нибудь неожиданные алиасы. И если вы это импортируете «как есть», можете случайно включить лишнее и получить половину приложения, собранную дважды.

Первая зона риска — конфликт имён бинов. XML любит явные id="...", Java config любит имена методов @Bean. Сканирование любит называть бины по правилам lowerCamelCase от имени класса. Всё это встречается в одном контейнере, а значит, столкновения имён — абсолютно реальная вещь. Самая безопасная привычка на миграции — сохранять имена при переносе и удалять старые объявления сразу, а не «потом когда-нибудь».

Вторая зона риска — дублирование scanning. Например, у вас в Java config уже есть @ComponentScan("com.example.contextflow"), а legacy XML вдруг содержит context:component-scan base-package="com.example.contextflow.infrastructure.notification". Итог: контейнер дважды попробует зарегистрировать одни и те же компоненты. Иногда вы это заметите сразу (по конфликту имен), иногда нет (если имена разные), и тогда вы получите «почему у меня два NotificationSender-а, я же добавлял один».

Третья зона риска — аннотационная инфраструктура и placeholder’ы. Если XML использует ${...} placeholders, он ожидает, что в контексте есть infrastructure, которая умеет их разрешать. В нашем проекте мы уже поднимали Environment и placeholder support в Java config, так что обычно всё будет хорошо. Но если вы импортируете XML-фрагмент в «голый» контекст без этих вещей, он может упасть на старте не потому, что XML плохой, а потому что ему не хватило инфраструктурных бинов.

И наконец, самая полезная дисциплина: держите legacy-модуль маленьким и изолированным. В идеале legacy XML должен описывать конкретные бины, а не «поднимать половину приложения». Тогда у вас есть шанс мигрировать его за несколько шагов без нервного тика.

Минимальный пример для ContextFlow: подключаем legacy как модуль

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApp {
    public static void main(String[] args) {
        // Поднимаем единый контекст: CoreConfig + мост к legacy XML
        try (var ctx = new AnnotationConfigApplicationContext(
                com.example.contextflow.config.core.CoreConfig.class,
                com.example.contextflow.config.legacybridge.LegacyBridgeConfig.class
        )) {
            // Быстрая sanity-check проверка: бин из XML действительно в контейнере
            System.out.println(ctx.containsBean("legacyNotificationSender")); // true
        }
    }
}

Если true, значит legacy бин реально стал частью контейнера. Дальше вы можете мигрировать его по одному шагу, не пересобирая приложение заново и не создавая отдельный мир для XML.

8. Типичные ошибки при смешанной конфигурации

Ошибка №1: импортировать XML, не понимая его границы.
Если вы подключаете legacy-файл в надежде «ну контейнер как-нибудь разберётся», вы сами лишаете себя главного преимущества Spring — предсказуемости. Правильный подход начинается с ответа на вопрос: за что отвечает этот XML-фрагмент и какие бины он должен дать контейнеру. Без этого легко случайно притащить лишний scanning, лишние placeholder’ы и половину инфраструктуры «в нагрузку».

Ошибка №2: держать один и тот же бин и в XML, и в Java config.
Это классика миграции. Вы перенесли <bean id="legacyNotificationSender" .../> в @Bean legacyNotificationSender(), но забыли удалить старое объявление из XML. В лучшем случае контейнер упадёт и честно скажет, что у него конфликт. В худшем — одно определение перезапишет другое, и вы получите ситуацию «у меня вроде новая конфигурация, но работает почему-то старая». Дисциплина простая: перенесли — удалили.

Ошибка №3: менять имя бина “чуть-чуть, чтобы красивее”.
Переименовать legacyNotificationSender в consoleNotificationSenderLegacy кажется невинным, пока вы не вспомните, что где-то в XML есть ref="legacyNotificationSender", а где-то в Java code может быть @Qualifier("legacyNotificationSender"). На миграции лучше сохранять имена как есть, а красоту наводить после того, как всё переведено и стабилизировано.

Ошибка №4: мигрировать и рефакторить дизайн одновременно.
Очень хочется вместе с переносом из XML заменить setter injection на constructor injection, переписать интерфейс, выкинуть пару классов и «сделать наконец нормально». Но тогда вы перестаёте понимать, что именно сломалось: миграция или рефакторинг. Сначала добейтесь эквивалентного поведения в Java config, а потом отдельным шагом улучшайте дизайн.

Ошибка №5: забыть, что импортированный XML может добавить новых кандидатов и сломать autowiring.
В ContextFlow уже есть места с несколькими реализациями интерфейсов. Импорт XML может незаметно добавить ещё одну реализацию, и ваш конструкторный injection начнёт падать с NoUniqueBeanDefinitionException. Это не повод «ненавидеть XML», это повод вспомнить инструменты: @Qualifier, @Primary, явные имена бинов и аккуратные границы scanning.

1
Задача
Spring Core, 23 уровень, 3 лекция
Недоступна
Подключение XML в Java config через `@ImportResource`
Подключение XML в Java config через `@ImportResource`
1
Задача
Spring Core, 23 уровень, 3 лекция
Недоступна
Частичная миграция bean-а из XML в Java config
Частичная миграция bean-а из XML в Java config
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ