JavaRush /Курсы /Spring Core /ContextFlow: FactoryBean

ContextFlow: FactoryBean и диагностика

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

1. Две задачи в support-слое

Если до этого момента ваш ContextFlow воспринимался как “честный учебный сервис”, то сегодня мы добавим две вещи, которые очень похожи на то, что встречается в реальных проектах. Первая — выбор нужной реализации по конфигурации без размазывания if/else по сервисам. Вторая — техническая диагностика контекста, но без того, чтобы бизнес-код учился “жить от контейнера”.

Представим две задачи на человеческом языке.

Первая задача: у нас есть интерфейс ReportFormatter и две реализации — условно “текстовый” и “CSV-подобный”. В разных профилях и окружениях нам хочется подсовывать разный формат отчёта, но так, чтобы ReportingService оставался максимально простым: он должен зависеть от контракта, а не от логики выбора реализации.

Вторая задача: иногда хочется быстро увидеть, в каком режиме поднялся контекст. Какие активные профили? Какое имя приложения? Может, даже какой формат отчёта выбрали? И хочется сделать это так, чтобы не писать в каждом сервисе “ну-ка дай мне Environment… а теперь getProperty(...)…”. Для этого мы заведём маленький диагностический bean, которому контейнер сам отдаст нужную служебную информацию.

Ниже — маленькая “карта” решений, чтобы было видно, кто за что отвечает:

Что хотим сделать Где это живёт Почему именно там
Выбрать реализацию ReportFormatter по свойству contextflow.report.format support.factory + config.reporting Это логика сборки/создания объектов, а не бизнес-сценарий
Вывести краткую сводку про контекст (имя bean-а, профили, пару properties) support.diagnostics + config.core Это техническая диагностика, и ей разрешено знать про Spring-окружение

2. ReportFormatter и выбор формата

Перед тем как мы добавим FactoryBean, важно ещё раз проговорить простую мысль: сервис должен делать свою работу, а не играть в “сборщика приложения”. Когда ReportingService начинает решать, какую реализацию форматтера выбрать, он берёт на себя лишнюю ответственность. Сначала это кажется “ну чего там, один if”, а потом внезапно становится “пять if, три свойства, два профиля и один баг, который появляется только по пятницам”.

Допустим, контракт форматтера у нас выглядит так (если он у вас уже есть — просто сверяйтесь по смыслу):

package com.example.contextflow.domain.ports;

import com.example.contextflow.domain.model.DailyReport;

public interface ReportFormatter {

    // Форматируем доменный отчёт в строковое представление (конкретный формат решает реализация)
    String format(DailyReport report);
}

Реализации обычно лежат где-то в инфраструктуре или рядом с reporting-частью. Для примера, пусть будет так:

package com.example.contextflow.infrastructure.reporting;

import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;

public class TextReportFormatter implements ReportFormatter {

    @Override
    public String format(DailyReport report) {
        // Простой “человеческий” формат: пригоден для логов и консоли
        return "DAILY REPORT: " + report;
    }
}

И вторая:

package com.example.contextflow.infrastructure.reporting;

import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;

public class CsvReportFormatter implements ReportFormatter {

    @Override
    public String format(DailyReport report) {
        // CSV-стиль: сначала заголовок, затем данные отчёта
        return """
                date,orderCount,cancelledCount,totalAmount
                """ + report;
    }
}

Теперь ключевой момент. ReportingService должен выглядеть примерно так: он получает один ReportFormatter и просто пользуется им, не решая “какой именно это форматтер”. Пример (упрощённый до сути):

package com.example.contextflow.application.reporting;

import com.example.contextflow.domain.model.DailyReport;
import com.example.contextflow.domain.ports.ReportFormatter;

public class ReportingService {

    private final ReportFormatter formatter;

    public ReportingService(ReportFormatter formatter) {
        // Важно: зависимость — от контракта, а не от конкретной реализации
        this.formatter = formatter;
    }

    public String format(DailyReport report) {
        // Бизнес-код “скучный”: просто вызывает контракт
        return formatter.format(report);
    }
}

Если вы сейчас чувствуете лёгкое разочарование в стиле “и всё?” — поздравляю, вы как раз увидели цель хорошего wiring: бизнес-код становится скучным. Скучный бизнес-код — это часто комплимент, а не оскорбление.

3. ReportFormatterFactoryBean

Сейчас мы подойдём к FactoryBean не как к “хитрому API”, а как к способу спрятать логику создания/выбора туда, где ей и место: в инфраструктуру. Представьте, что ReportFormatterFactoryBean — это бариста. Бариста сам не является кофе, но именно он решает, будет вам латте или эспрессо, и отдаёт результат, а не себя самого.

Создадим фабрику в support.factory. Она будет зависеть только от контрактов и реализаций форматтеров, а не от бизнес-сервисов.

package com.example.contextflow.support.factory;

import org.springframework.beans.factory.FactoryBean;

import com.example.contextflow.domain.ports.ReportFormatter;
import com.example.contextflow.infrastructure.reporting.CsvReportFormatter;
import com.example.contextflow.infrastructure.reporting.TextReportFormatter;

public class ReportFormatterFactoryBean implements FactoryBean<ReportFormatter> {

    // Значение по умолчанию: если свойство не задано, берём “text”
    private String format = "text";

    public void setFormat(String format) {
        // Spring установит это значение из properties через конфигурацию
        this.format = format;
    }

    @Override
    public ReportFormatter getObject() {
        // Здесь и живёт логика выбора конкретной реализации (а не в сервисе)
        return "csv".equalsIgnoreCase(format)
                ? new CsvReportFormatter()
                : new TextReportFormatter();
    }

    @Override
    public Class<?> getObjectType() {
        // Сообщаем контейнеру тип “продукта”, который производит фабрика
        return ReportFormatter.class;
    }

    @Override
    public boolean isSingleton() {
        // Один форматтер на всё приложение — логично для данного кейса
        return true;
    }
}

Здесь важно две вещи.

Во-первых, consumer по-прежнему видит только ReportFormatter: FactoryBean остаётся в support, а наружу контейнер отдаёт результат getObject().

Во-вторых, контейнеру нужна служебная информация о produced object — для этого и существуют getObjectType() и isSingleton(). Благодаря этому ReportingService не знает ни про TextReportFormatter, ни про CsvReportFormatter, ни про сам ReportFormatterFactoryBean. Он получает один контракт и работает дальше.

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

4. Конфигурация и отличие фабрики от продукта

Теперь нам нужно встроить фабрику в конфигурацию так, чтобы формат выбирался из properties. Здесь мы используем уже знакомую технику: @Configuration, @Bean и @Value с дефолтом. Главное — аккуратно выбрать имя bean-а, потому что в мире FactoryBean имя становится особенно заметным.

Создадим модуль config.reporting.ReportingConfig:

package com.example.contextflow.config.reporting;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.contextflow.support.factory.ReportFormatterFactoryBean;

@Configuration
public class ReportingConfig {

    @Bean
    public ReportFormatterFactoryBean reportFormatter(
            @Value("${contextflow.report.format:text}") String format) {

        // Создаём фабрику и передаём ей настройку формата из properties
        ReportFormatterFactoryBean factory = new ReportFormatterFactoryBean();
        factory.setFormat(format);
        return factory;
    }
}

Обратите внимание на имя метода reportFormatter(...). Для Spring это имя становится bean name. И вот тут начинается интересное: с точки зрения контейнера bean с именем reportFormatter — это FactoryBean, но с точки зрения потребителей reportFormatter ведёт себя как ReportFormatter.

То есть такой код будет работать (и отдаст produced object):

// Получаем продукт фабрики (реальный ReportFormatter), а не саму фабрику
ReportFormatter formatter = context.getBean("reportFormatter", ReportFormatter.class);

А вот если вы захотите получить сам объект фабрики (например, чисто для диагностики), используется специальный префикс &. Да, это именно та самая “магическая амперсанда”, которая нужна ровно в одном месте: когда вы хотите factory-object, а не produced-object.

// Получаем именно объект фабрики (FactoryBean), а не произведённый форматтер
Object factory = context.getBean("&reportFormatter");

Чтобы это почувствовать руками, можно сделать маленькую проверку прямо в main() на этапе запуска (и это как раз тот случай, когда getBean() допустим: мы в bootstrap/diagnostic code, а не в бизнес-сервисе):

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.example.contextflow.config.core.ContextFlowAppConfig;
import com.example.contextflow.domain.ports.ReportFormatter;

try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {

    // По имени "reportFormatter" получаем продукт фабрики (TextReportFormatter или CsvReportFormatter)
    ReportFormatter formatter = context.getBean("reportFormatter", ReportFormatter.class);
    System.out.println(formatter.getClass().getSimpleName()); // TextReportFormatter

    // С префиксом "&" получаем сам объект фабрики
    Object factory = context.getBean("&reportFormatter");
    System.out.println(factory.getClass().getSimpleName());   // ReportFormatterFactoryBean
}

Теперь про properties. В базовом contextflow.properties (или profile-specific файле) у вас появится настройка:

# По умолчанию используем текстовый формат
contextflow.report.format=text

А, например, в contextflow-demo.properties можно сделать:

# В demo-профиле хотим CSV
contextflow.report.format=csv

И вы получите простую и приятную вещь: один и тот же ReportingService остаётся неизменным, а вот состав и поведение приложения в части форматирования отчёта меняется конфигурацией.

5. ContextDiagnosticsBean для диагностики

С диагностикой держим ту же дисциплину. Для проекта зафиксируем явный infrastructure-bean, зарегистрированный через config: так Aware остаётся в support-слое и не выглядит случайным @Component, который просто нашёлся scanning-ом.

Это полезно, когда приложение становится profile-aware и property-heavy: при старте вы сразу видите, в каком режиме поднялся контекст и какой конфиг реально подхватился. Такой bean не должен обрабатывать заказы и не должен искать зависимости. Его роль — смотреть и сообщать.

Сделаем класс в support.diagnostics:

package com.example.contextflow.support.diagnostics;

import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;

public class ContextDiagnosticsBean implements BeanNameAware, EnvironmentAware {

    private String beanName;
    private Environment environment;

    @Override
    public void setBeanName(String name) {
        // Контейнер сообщает bean-у его имя
        this.beanName = name;
    }

    @Override
    public void setEnvironment(Environment environment) {
        // Контейнер отдаёт доступ к Environment (profiles + properties)
        this.environment = environment;
    }

    public String summary() {
        // Берём пару свойств с дефолтами — чисто для быстрой диагностики
        String appName = environment.getProperty("contextflow.app-name", "ContextFlow");
        String format = environment.getProperty("contextflow.report.format", "text");

        String profiles = String.join(",", environment.getActiveProfiles());
        if (profiles.isBlank()) {
            // Если активных профилей нет, считаем, что работаем в дефолтном режиме
            profiles = "default";
        }

        // Возвращаем одну строку, удобную для лога на старте
        return "[" + beanName + "] appName=" + appName
                + ", profiles=" + profiles
                + ", report.format=" + format;
    }
}

Здесь мы сделали два Aware-интерфейса, которые чаще всего оказываются полезными в диагностике.

BeanNameAware позволяет контейнеру сказать bean-у: “твое имя вот такое”. Это удобно, чтобы в больших конфигурациях не теряться (особенно если вы используете aliases или явные имена). EnvironmentAware отдаёт нам Environment, который мы отлично знаем с дней про properties, profiles и внешнюю конфигурацию. То есть мы не “влезли в контейнер”, мы просто получили официально переданный объект окружения.

Зарегистрируем диагностический bean через config (в явной форме — чтобы было видно, что это инфраструктура, а не “просто случайный компонент, который нашёлся сканированием”):

package com.example.contextflow.config.core;

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

import com.example.contextflow.support.diagnostics.ContextDiagnosticsBean;

@Configuration
public class DiagnosticsConfig {

    @Bean
    public ContextDiagnosticsBean contextDiagnosticsBean() {
        // Явно создаём диагностический bean (инфраструктура/поддержка)
        return new ContextDiagnosticsBean();
    }
}

В проекте фиксируем именно этот вариант: отдельный support-bean + явная регистрация через DiagnosticsConfig.

И теперь в bootstrap-коде можно сделать так:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.example.contextflow.config.core.ContextFlowAppConfig;
import com.example.contextflow.support.diagnostics.ContextDiagnosticsBean;

try (var context = new AnnotationConfigApplicationContext(ContextFlowAppConfig.class)) {
    // Это bootstrap/diagnostic code: getBean() здесь уместен
    ContextDiagnosticsBean diag = context.getBean(ContextDiagnosticsBean.class);
    System.out.println(diag.summary());
    // [contextDiagnosticsBean] appName=ContextFlow, profiles=dev, report.format=text
}

Эта строка в начале запуска — как наклейка на коробке: “внутри именно то, что вы хотели”. И да, иногда это спасает больше времени, чем самый умный дебаггер: дебаггер хорош, но он не умеет читать ваши мысли о том, какой профиль вы “точно включили”.

6. Общая картина и структура пакетов

Теперь соберём всё в одну понятную картину: где эти классы лежат, кто кого знает, и почему это не превращает ContextFlow в “приложение, которое думает только о Spring”. В этом месте полезно посмотреть на проект как на карту города: бизнес-улицы — в domain и application, технические коммуникации — в support и config.

Если опираться на целевую структуру курса, нужные нам части выглядят так:

com.example.contextflow
├── application
│   └── reporting
│       └── ReportingService
├── domain
│   └── ports
│       └── ReportFormatter
├── infrastructure
│   └── reporting
│       ├── TextReportFormatter
│       └── CsvReportFormatter
├── support
│   ├── factory
│   │   └── ReportFormatterFactoryBean
│   └── diagnostics
│       └── ContextDiagnosticsBean
└── config
    ├── core
    │   ├── DiagnosticsConfig
    │   └── ContextFlowAppConfig
    └── reporting
        └── ReportingConfig

Чтобы это ещё лучше уложилось, вот маленькая схема зависимостей (без “лишних стрелок”):

flowchart TD
  RS["ReportingService
(application)"] --> RF["ReportFormatter
(port)"] subgraph Support["support слой"] FBF["ReportFormatterFactoryBean
(FactoryBean)"] -->|"getObject()"| RF DIAG["ContextDiagnosticsBean
(Aware)"] end subgraph Config["config слой"] RC["ReportingConfig"] --> FBF DC["DiagnosticsConfig"] --> DIAG end ENV["Environment
(properties + profiles)"] --> RC ENV --> DIAG

Обратите внимание на полезную “тишину” в этой схеме: ReportingService не знает ни про фабрику, ни про Environment, ни про ApplicationContext. Он знает только про ReportFormatter, и это именно то состояние, к которому мы стремимся.

А ContextDiagnosticsBean знает про Environment, но это нормально, потому что его работа — диагностика окружения. Он не выбирает формат отчёта, он его показывает. Выбор делает конфигурация + фабрика.

На практике вы увидите примерно такой эффект: при старте приложения в профиле dev диагностика скажет report.format=text, а при старте в demoreport.format=csv. И ReportingService при этом останется одним и тем же классом, без единой строчки “если demo — то …”.

Это и есть рабочий snapshot ContextFlow: выбор форматтера и диагностика живут в support/config, а ReportingService остаётся обычным consumer-ом ReportFormatter.

7. Типичные ошибки при FactoryBean и Aware

Ошибка №1: делать ReportingService зависимым от ReportFormatterFactoryBean.
Это очень соблазнительно: “ну раз фабрика умеет делать форматтер, давайте я её и заинжекчу”. Но тогда ваш сервис начинает знать про контейнерный механизм. У него появится знание “у меня есть фабрика”, а значит вы теряете главную пользу FactoryBean: consumer должен видеть только produced object (ReportFormatter) и жить спокойной жизнью.

Ошибка №2: не понимать, что по имени bean-а вы получаете produced object, а не фабрику.
Классическая ситуация: вы пишете context.getBean("reportFormatter"), ожидаете фабрику, а получаете TextReportFormatter. Это не “Spring сломался”, это его нормальная логика для FactoryBean. Если вам реально нужен объект фабрики, используйте &reportFormatter. И старайтесь, чтобы этот трюк оставался в диагностике/инфраструктуре, а не переезжал в бизнес-слой.

Ошибка №3: забыть корректно реализовать getObjectType() и получить странные wiring-ошибки.
Если getObjectType() возвращает слишком общий тип, null или что-то не то, контейнеру сложнее делать автосвязывание и диагностику. В учебном проекте это проявится как “почему контейнер не видит мой ReportFormatter”, а в реальном проекте — как “почему это работает только в одном профиле”. Держите этот метод честным: если фабрика производит ReportFormatter, пусть так и скажет.

Ошибка №4: использовать Aware как замену constructor injection.
Aware — это не “более мощная инъекция”, это специальный канал для контейнерной информации. Как только вы начинаете делать ApplicationContextAware в обычном сервисе “чтобы не писать конструктор”, вы отрезаете себе прозрачность зависимостей и возвращаетесь к скрытому графу. В итоге класс выглядит простым, но внутри у него спрятан доступ к миру.

Ошибка №5: превращать ContextDiagnosticsBean в “бизнес-сервис с правами администратора”.
Иногда диагностический bean начинает обрастать методами “а давай-ка я ещё создам заказ”, “а давай-ка я подменю формат отчёта на лету”. Это уже не диагностика, это второй слой приложения с непонятной ответственностью. Диагностический bean должен быть скучным: прочитал пару свойств, выдал строку, максимум — проверил наличие каких-то условий.

1
Задача
Spring Core, 20 уровень, 4 лекция
Недоступна
Мини-модуль отчётов с `FactoryBean` и diagnostics-bean
Мини-модуль отчётов с `FactoryBean` и diagnostics-bean
1
Задача
Spring Core, 20 уровень, 4 лекция
Недоступна
Мини-модуль уведомлений с изолированным support-слоем
Мини-модуль уведомлений с изолированным support-слоем
1
Опрос
Spring бины, 20 уровень, 4 лекция
Недоступен
Spring бины
Фабрики, callbacks и DI
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ