1. Чтение зависимостей важнее тегов
Если мысль «XML — это тот же контейнер, только другой синтаксис» уже улеглась, дальше нужен более приземлённый навык: читать зависимости без паники. Когда впервые открываешь legacy XML, мозг часто реагирует так: «Ого, тут простыня, тут xmlns, тут какие-то схемы… Я сдаюсь». Это нормальная реакция: XML любит быть многословным, даже когда речь идёт об одном-единственном bean-е. Но наша цель сегодня не выучить XML как язык, а научиться видеть в нём то, что вы уже знаете: кто создаётся, как называется и от кого зависит.
Хорошая новость: в большинстве небольших legacy-файлов вам не нужно «понимать всё». Вам нужно восстановить wiring-картину: какие beans объявлены, какие из них ссылаются на другие, где конструкторная инъекция, где setter-инъекция, и какие значения передаются как строки/числа, а какие — как ссылки на другие beans. Это примерно как читать электрическую схему: вас не интересует, из какого материала провода, но вы хотите знать, где плюс, где минус и почему лампочка не горит.
Чтобы читать XML не «на глазок», удобно держать в голове один простой перевод.
| То, что вы ищете в XML | Как это звучит по-нашему, по Spring |
|---|---|
|
«Контейнеру сообщили: вот BeanDefinition такого-то класса» |
|
«Это имя bean-а, по нему будут ссылаться через ref» |
|
«Зависимость приходит через конструктор (constructor injection)» |
|
«Зависимость/настройка задаётся через setter (property injection)» |
|
«Ссылка на другой bean по имени» |
|
«Простое значение (строка/число/и т.п.), не bean» |
Этот перевод мы и будем тренировать дальше.
2. <beans>: корневой элемент и шапка
Почти любой Spring XML начинается с тега <beans>, и почти любой новичок пытается по нему понять смысл файла — и неизбежно страдает. В шапке много «служебного»: namespace-ы, xsi:schemaLocation и прочие вещи, которые не говорят вам, какой AuditWriter используется в проекте. Но шапка важна как паспорт файла: без неё файл иногда просто не прочитается контейнером или будет невалидным.
Минимальная «здоровая» шапка для самого базового beans-namespace выглядит так:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Внутри: определения бинов (BeanDefinition), которые прочитает контейнер -->
<!-- Дальше будут <bean> ... -->
</beans>
Здесь полезно понять ровно две вещи (без углубления в XML-теорию). Во‑первых, xmlns=".../beans" означает: «внутри этого файла по умолчанию используются теги из beans-схемы», то есть <bean>, <property>, <constructor-arg> и т.д. Во‑вторых, schemaLocation указывает, где лежит XSD-схема, чтобы IDE могла подсказать вам атрибуты и чтобы валидатор понимал, что такое <bean>.
Инженерный лайфхак чтения: когда вы открыли XML, первый раз шапку можно пролистать глазами за 1–2 секунды. Если вы видите только beans-namespace — отлично, сегодня это наш случай. Если видите дополнительные namespaces (например, context:), не паникуйте, но и не бросайтесь их разбирать прямо сейчас: мы сознательно учимся идти от базового.
3. <bean>: имя и класс bean-а
Теперь про самое вкусное: тег <bean> — это и есть XML-эквивалент того, что вы раньше делали через @Component или @Bean. Он описывает контейнеру один bean definition: какой класс создавать, как его назвать, и (опционально) как его собрать.
Самая минимальная форма — «создай экземпляр этого класса и положи в контекст под таким именем»:
<bean id="auditWriter"
class="com.example.contextflow.infrastructure.audit.ConsoleAuditWriter"/>
Перевод на человеческий: «В контейнере должен быть bean с именем auditWriter и классом ConsoleAuditWriter».
Если вспомнить Java config, смысл очень похож на такой код:
import com.example.contextflow.infrastructure.audit.ConsoleAuditWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // Говорим Spring: это конфигурационный класс
class AuditConfig {
@Bean // Регистрируем bean с именем метода: auditWriter
ConsoleAuditWriter auditWriter() {
// Контейнер вызовет этот метод и положит результат в контекст
return new ConsoleAuditWriter();
}
}
Разница только в синтаксисе. И в Java config, и в XML контейнер сначала регистрирует метаданные (BeanDefinition), а реальные объекты создаёт позже, когда контекст «refresh-ится» и создаёт singleton-бины.
Важно не пропустить, что id в XML — это не просто «красивое имя». Это ключ, по которому будут ссылаться другие beans через ref. Поэтому в legacy XML чаще явно пишут id, даже если теоретически можно оставить без него и позволить Spring сгенерировать имя. На практике люди предпочитали не играть в «угадай, какое имя сгенерировал контейнер».
Если хотите чуть лучше ориентироваться в реальном старом коде, полезно знать, что в XML иногда вместо id используют name. В упрощённом виде это можно воспринимать так: id — основное имя, name — тоже имя (и иногда набор алиасов). Но для чтения маленьких фрагментов достаточно помнить простое правило: ищите, как bean назван, и как на него ссылаются.
4. <constructor-arg>: конструкторная инъекция
После <bean> следующий ключевой тег — <constructor-arg>. Он показывает, что зависимости (или значения) передаются в конструктор. Для нас это прямо родная тема: весь курс мы вас приучали к constructor injection как к понятному, fail-fast и читаемому стилю. В XML это выглядит чуть многословнее, но смысл идентичен.
Представим, что у нас есть сервис аудита, который зависит от AuditWriter:
import com.example.contextflow.domain.ports.AuditWriter;
public class AuditService {
private final AuditWriter auditWriter; // Зависимость, которую должен дать контейнер
public AuditService(AuditWriter auditWriter) {
// Constructor injection: без зависимости объект не соберётся
this.auditWriter = auditWriter;
}
}
В XML такой wiring чаще всего выглядит так:
<bean id="auditService"
class="com.example.contextflow.application.service.AuditService">
<!-- Внедряем зависимость через конструктор по имени bean-а -->
<constructor-arg ref="auditWriter"/>
</bean>
Здесь ref="auditWriter" означает: «подставь в конструктор bean с именем auditWriter». И обратите внимание на важный момент: XML читает зависимости по именам, а не «по типу, как автосвязывание». Контейнер, конечно, проверит, что bean подходит по типу параметра конструктора, но сам факт ссылки задаётся именем. Поэтому для чтения legacy-файла имя — это ваша ниточка Ариадны.
Чуть более жизненный пример — когда конструктор принимает и ссылку на другой bean, и какое-то простое значение. Допустим, legacy-класс пишет аудит в файл и ему нужен путь:
import java.nio.file.Path;
public class LegacyFileAuditWriter {
private final Path outputDir; // Настройка (не bean), обычно приходит как value
public LegacyFileAuditWriter(Path outputDir) {
// Spring попробует сконвертировать строку из XML в Path
this.outputDir = outputDir;
}
}
В XML это может быть описано так:
<bean id="fileAuditWriter"
class="com.example.contextflow.infrastructure.audit.LegacyFileAuditWriter">
<constructor-arg value="build/audit"/>
</bean>
Здесь вместо ref — value, потому что это не ссылка на другой bean, а «просто значение». И да, оно текстовое: контейнер потом попробует сконвертировать строку в нужный тип (в нашем курсе вы уже видели, что Spring умеет conversion и можно добавлять свои converter-ы).
В реальном legacy-коде вы можете встретить случаи, когда у <constructor-arg> дополнительно стоят уточнения index, name или type. Они нужны, когда конструктор перегружен или когда аргументов много, и автор конфигурации не хотел полагаться на порядок.
Вот пример (для чтения, не для того, чтобы вы прямо завтра так писали):
<bean id="auditService"
class="com.example.contextflow.application.service.AuditService">
<constructor-arg index="0" ref="auditWriter"/>
</bean>
Смысл тот же, но явно указано: это первый параметр конструктора. При чтении таких фрагментов воспринимайте эти атрибуты как подсказку: «тут кто-то уже обжёгся на неоднозначности».
5. <property>: setter/property injection в legacy
После недели (ну ладно, после курса) жизни с constructor injection XML с <property> может выглядеть как привет из прошлого: «Почему они не передали зависимость в конструктор, а засунули её в setter?» Ответ простой: исторически так было принято, и огромное количество legacy-фрагментов именно так и устроены. Наша задача — читать и понимать, а не спорить с археологией.
<property> означает: «после создания объекта (конструктором без аргументов или с частью аргументов) контейнер вызовет setter и установит значение свойства». Это требует, чтобы у класса был JavaBean-сеттер.
Пример простого класса с setter:
import java.nio.file.Path;
public class ReportOutputManager {
private Path outputDir; // Это поле будет установлено ПОСЛЕ конструктора
public void setOutputDir(Path outputDir) {
// Property injection: контейнер вызовет этот setter при сборке bean-а
this.outputDir = outputDir;
}
}
Тогда XML-конфигурация может выглядеть так:
<bean id="reportOutputManager"
class="com.example.contextflow.support.lifecycle.ReportOutputManager">
<!-- name соответствует JavaBean-свойству: ожидается setOutputDir(...) -->
<property name="outputDir" value="build/reports"/>
</bean>
Ключевой момент чтения здесь — связь между name="outputDir" и Java-классом. Spring ожидает JavaBean property с именем outputDir, то есть метод setOutputDir(...). Если вы в XML напишете name="outputDIR" или name="outDir", а такого setter-а нет, контейнер не будет «догадываться» и свяжет это с ошибкой.
Это очень полезная дисциплина чтения: каждый <property name="..."> можно мысленно переписать как «контейнер вызовет setXxx(...)». И тогда вы сразу понимаете, что должно существовать в классе.
Ещё одна важная деталь: <property> может использоваться и вместе с <constructor-arg>. То есть часть зависимостей приходит через конструктор, а часть — через setters. Когда вы это видите, думайте так: «объект создаётся, потом докручивается настройками». В legacy это часто использовалось для опциональных вещей, которые хотелось включать/выключать конфигурацией.
6. ref и value: ссылки и значения
ref и value — это буквально два способа сказать контейнеру «что подставить». Но они отвечают на разные вопросы. ref говорит: «подставь другой bean из контейнера», а value говорит: «подставь literal-значение». Новичок в XML чаще всего путается именно тут, потому что визуально это выглядит как два похожих атрибута, а на деле за ними разные миры.
Посмотрим на два почти одинаковых фрагмента:
<property name="messageSource" ref="messageSource"/>
и
<property name="outputDir" value="build/reports"/>
В первом случае messageSource — это ссылка на другой bean. Во втором — строка, которую Spring попытается сконвертировать в нужный тип (например, Path). В ContextFlow это особенно важно, потому что у нас уже есть и инфраструктурные beans (MessageSource, template catalogs, output managers), и настройки, которые приходят из properties. В XML-ветке вы можете встретить и то, и другое, вперемешку.
Очень полезно читать так: если это объект приложения, у которого есть собственный класс и жизненный цикл в контейнере — это почти всегда ref. Если это «настройка» (путь, флаг, число, имя приложения) — это почти всегда value.
А теперь маленький, но приятный факт: в XML порядок объявления beans обычно не важен. Вы можете ссылаться через ref на bean, который объявлен ниже по файлу. Контейнер сначала регистрирует definitions, а потом создаёт объекты (мы это разбирали ещё в самом начале курса). Поэтому «ссылка вперёд» — нормально.
7. init-method и destroy-method: lifecycle callbacks
После того как вы научились читать зависимости, следующий слой понимания — lifecycle. В XML он часто виден прямо в атрибутах <bean>. Это очень похоже на то, как в Java config можно написать @Bean(initMethod = "...", destroyMethod = "..."), только здесь это делается в XML.
Пример: допустим, у нас есть каталог шаблонов уведомлений, который на старте загружает шаблоны, а при shutdown очищает кэш:
public class NotificationTemplateCatalog {
public void loadTemplates() {
// init-method: будет вызван контейнером после создания и внедрения зависимостей
System.out.println("Templates loaded"); // Templates loaded
}
public void clearCache() {
// destroy-method: будет вызван контейнером при закрытии контекста
System.out.println("Cache cleared"); // Cache cleared
}
}
Тогда XML может выглядеть так:
<bean id="templateCatalog"
class="com.example.contextflow.infrastructure.resources.NotificationTemplateCatalog"
init-method="loadTemplates"
destroy-method="clearCache"/>
Чтение здесь прямолинейное: после инъекций контейнер вызовет loadTemplates(), а при закрытии контекста вызовет clearCache().
И вот тут «legacy-подстава», о которую спотыкаются даже те, кто уже видел lifecycle в аннотациях: destroy-method сработает только если контекст реально закрыли. Поэтому в примерах (и в реальной жизни) вы увидите старый добрый try-with-resources:
import org.springframework.context.support.ClassPathXmlApplicationContext;
try (var context = new ClassPathXmlApplicationContext("legacy/legacy-audit-context.xml")) {
// Контекст поднялся, все singleton-бины создались (если они eager), init-method-ы отработали
System.out.println("Context started"); // Context started
} // Здесь context.close() вызовется автоматически, и destroy-method-ы реально отработают
Здесь важно не то, что контекст XML-овый, а то, что он закрывается автоматически — и destroy callbacks реально выполнятся. Это та же дисциплина, которую вы уже выучили на AnnotationConfigApplicationContext.
8. Как читать XML последовательно
Сейчас соберём всё в одну мини-картину. Представьте, что вы открыли файл src/main/resources/legacy/legacy-mini-context.xml и хотите понять, что он делает. Вот небольшой пример, где есть и конструкторная связь, и property-настройка, и lifecycle:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Простой bean без зависимостей -->
<bean id="auditWriter"
class="com.example.contextflow.infrastructure.audit.ConsoleAuditWriter"/>
<!-- Зависимость через constructor injection: auditService -> auditWriter -->
<bean id="auditService"
class="com.example.contextflow.application.service.AuditService">
<constructor-arg ref="auditWriter"/>
</bean>
<!-- Настройка через setter/property injection -->
<bean id="reportOutputManager"
class="com.example.contextflow.support.lifecycle.ReportOutputManager">
<property name="outputDir" value="build/reports"/>
</bean>
<!-- Lifecycle callbacks на старте и при остановке -->
<bean id="templateCatalog"
class="com.example.contextflow.infrastructure.resources.NotificationTemplateCatalog"
init-method="loadTemplates"
destroy-method="clearCache"/>
</beans>
Как это читать без героизма и гаданий? Я бы читал примерно так, почти вслух.
Сначала фиксируем границу: это обычный beans-файл, без дополнительных namespaces. Значит, внутри в основном ручные определения <bean>. Затем выписываем «словарь»: какие ids объявлены и какие у них классы. Уже на этом шаге у вас появляется список из четырёх объектов, которые гарантированно попадут в контейнер.
Потом читаем связи. У auditService есть <constructor-arg ref="auditWriter">, значит AuditService зависит от ConsoleAuditWriter через интерфейс AuditWriter. У reportOutputManager есть <property name="outputDir" value="...">, значит объект создастся и потом получит настройку через setter setOutputDir(...). И наконец, templateCatalog имеет init-method и destroy-method, то есть с ним связаны lifecycle callbacks.
Если вам помогает визуализация, можно мысленно превратить это в граф:
graph TD
auditService["auditService : AuditService"] --> auditWriter["auditWriter : ConsoleAuditWriter"]
reportOutputManager["reportOutputManager : ReportOutputManager"] --> outputDir["value: build/reports"]
templateCatalog["templateCatalog : NotificationTemplateCatalog"] --> init["init: loadTemplates()"]
templateCatalog --> destroy["destroy: clearCache()"]
Да, диаграмма выглядит немного «учебно». Но цель — научиться видеть причинно-следственную связь. Когда вы так прочитали файл, он перестал быть загадкой. Это просто описание графа объектов и их настроек.
9. Типичные ошибки при чтении XML-зависимостей
Ошибка №1: путать ref и value, потому что «ну это же оба атрибута».
Почти всегда это кончается тем, что вместо ссылки на bean вы передали строку, или наоборот. Если поставить value="auditWriter" там, где нужен ref="auditWriter", вы буквально передадите строку "auditWriter" в параметр конструктора, и дальше контейнер будет пытаться сделать из этой строки AuditWriter. Спойлер: не сделает. Если наоборот поставить ref="build/reports", Spring начнёт искать bean с именем build/reports и упадёт с ошибкой «такого bean-а нет». Спасает простая дисциплина: ref — это «другой bean», value — это «настройка».
Ошибка №2: считать, что <property name="..."> — произвольная строка, а не имя JavaBean-свойства.
<property name="outputDir"> — это не «ключ», как в map. Это указание на setter: Spring ожидает setOutputDir(...). Если такого метода нет, вы получите вполне честную ошибку о том, что свойство нельзя записать. При чтении legacy всегда полезно открыть класс и найти соответствующий setter — тогда всё становится на свои места.
Ошибка №3: не замечать форму внедрения и делать неверные выводы о дизайне класса.
Один и тот же класс в XML может получать часть зависимостей в конструктор, а часть — через setters. Если вы читаете быстро, вы можете решить, что объект «создаётся полностью» в конструкторе, хотя на деле после него ещё идёт пачка property. Это важно, потому что такие объекты нередко зависят от порядка инициализации и могут вести себя иначе до вызова init-method. При чтении старайтесь всегда мысленно проговаривать: «создали → внедрили свойства → вызвали init».
Ошибка №4: думать, что порядок <bean> в файле влияет на возможность ссылок.
Интуитивно кажется, что bean должен быть объявлен «выше», прежде чем на него можно сослаться. В Spring это не так: контейнер сначала собирает определения, потом создаёт экземпляры. Поэтому ref на bean, который написан ниже, — абсолютно нормальная история. Если файл не работает, причина почти никогда не в том, что «bean ниже», а в том, что имя не совпало или класс не подходит по типу.
Ошибка №5: пропускать init-method/destroy-method, а потом удивляться «странному поведению» на старте и при остановке.
Иногда bean ведёт себя так, будто у него есть магическая подготовка данных, хотя «в коде конструктора ничего нет». А потом вы замечаете init-method="loadTemplates", и всё становится логичным: это lifecycle. Точно так же можно часами думать, почему «не закрываются ресурсы», хотя в XML есть destroy-method, но контекст не закрывается. При чтении XML после зависимостей всегда полезно пробежать глазами атрибуты lifecycle — это часто ключ к пониманию поведения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ