Когда начинал разработку системы многомерного анализа данных временных рядов Dimension-UI, для внедрения зависимостей в исходном коде решил использовать Dagger 2. Практический опыт показал, что для приложений с большим количеством динамически создаваемых объектов инверсия зависимостей, реализованная в Dagger 2, не подходит.

Да, создание графа зависимостей в compile-time — это, во-первых, очень быстро, и, во-вторых, удобно: получаешь сообщения об ошибках конфигурации уже при компиляции.

Но накладные расходы на сопровождение всего этого хозяйства – прямо скажем, это боль.

Чтобы реализовать scope-зависимости, приходится писать и поддерживать много инфраструктурного кода внутри объектов, куда мы внедряем зависимости. В Dagger 2 такая реализация, во-первых, «загрязняет» код, а во-вторых, серьезно осложняет тестирование. Изолировать методы удобным способом не получается: в тестах нужно писать очень много кода, чтобы прокинуть необходимый контекст и корректно мокировать внешние зависимости. Я туда просто не полез — покрывал unit- и UI-тестами только базовую функциональность, где были Singleton-зависимости.

Даже с одними Singleton’ами приходится поднимать отдельную тестовую инфраструктуру для запуска приложения в тестовом режиме. Это не просто неудобно — это очень затратно по времени. Если сравнить усилия, которые надо потратить на реализацию тестирования подобного функционала в Spring и Dagger… Сравнение будет не в пользу Dagger. В целом я начал думать о переходе на runtime-генерацию графа зависимостей.

Поиск альтернатив

Итак, мы попробовали compile-time генерацию графа зависимостей в Dagger 2 — и нам это не подходит. Какие альтернативы? Если смотреть глобально, остаются Spring и Guice. Spring реализует runtime-генерацию графа зависимостей через рефлексию. Spring не рассматриваем — это большая и массивная библиотека; тащить её в наш tiny and cozy проект не очень правильно (плюс вопросы производительности/оптимизации).

Остается Guice — он тоже runtime (через рефлексию), вроде подходит. Смотрим активность проекта на GitHub: последний релиз май 2023 года — это жжж неспроста. Я понимаю, время было непростое для Google, но сам факт намекает на охлаждение интереса к направлению. Косвенно это подтверждает движение Micronaut и Quarkus в сторону compile-time контейнеров DI.

Хорошо, «большая тройка» лидеров — с ними понятно. Есть и нишевые решения. Честно говоря, я смотрел их по верхам — просто не было времени. Полная таблица со сравнением есть в конце статьи, здесь приведу предварительный итог:

  • Spring/Guice: мейнстрим enterprise-разработка, runtime;

  • PicoContainer: нишевый инструмент для специфических задач, runtime;

  • HK2: специализированный инструмент для JAX-RS/OSGi-экосистем, runtime;

  • Avaje Inject: современная альтернатива Micronaut/Quarkus с упором на простоту, compile-time

Все варианты с runtime построением графа зависимостей используют Java Reflection API.

Из предложенных альтернатив более-менее близок был PicoContainer, но инвестировать время в инструмент, у которого на GitHub затишье, я не решился.

Это все понятно, вроде как разложено по полочкам (что-то я разложил сейчас, когда писал эту статью). Но в основном, у меня были ощущения, что торопиться в этом деле не нужно.

Все-таки надо попробовать разведать вариант с написанием собственного DI – с учетом того что по любому, в новых версиях Java должна быть какая возможность сделать runtime DI основанных на других принципах.

Помощь магистров искусственного разума LLM

Решил обратиться к трем LLM (gpt-5-high, gemini-2.5-pro и deepseek-v3.2-exp-thinking) с вопросом, как можно реализовать DI на runtime с поддержкой JSR-330 в последних версиях Java. С задачей справился только Google Gemini и в одном из вариантов предложил формировать граф зависимостей, сканируя class-файлы на наличие аннотаций JSR-330 с помощью Class-File API (JEP 484, JDK 24+).

По сути это та же схема, что и с Dagger 2, но в runtime и с минимальным использованием рефлексии — с поддержкой функций @Singleton, @Named и @Provides. Все с прицелом н�� простую миграцию с Dagger 2 и с минимальными правками в коде. Без использования устаревшего javax.inject: вместо него — jakarta.inject обновленная версия стандарта JSR-330.

После нескольких итераций, генерации тестов, документации - на выходе получился легковесный контейнер для внедрения зависимостей, оптимизированный для производительности и простоты использования – Dimension-DI.

  • Использует JSR-330 (@Inject, @Named) для чистого кода с внедрением через конструктор;

  • Поддерживает scope @Singleton, обнаружение циклических зависимостей и явную привязку для интерфейсов.

  • Использует сканирование classpath через JDK Class-File API (без загрузки классов) для быстрого запуска.

  • Никаких прокси, генерации байт-кода или магии во время выполнения — только простой, потокобезопасный сервис-локатор "под капотом".

Схема работы Dimension-DI
Схема работы Dimension-DI
  1. Конфигурация на этапе сборки: Гибкий API Builder используется для настройки DI-контейнера. Этот этап включает сканирование classpath на наличие компонентов, помеченных @Inject, анализ их зависимостей и регистрацию провайдеров (рецептов для создания объектов). Именно здесь проявляется "DI" часть.

  2. Разрешение во время выполнения: Во время выполнения зависимости разрешаются с помощью внутреннего, глобально доступного ServiceLocator. Хотя реализация использует Service Locator, дизайн настроен на написание кода вашего приложения с использованием чистого Внедрения через конструктор (Constructor Injection), отделяя ваши компоненты от самого DI-фреймворка. Можно напрямую вызывать ServiceLocator - но только в отдельных случаях, об этом ниже.

Смена DI в Dimension-UI

Процесс перехода Dimension-UI с Dagger 2 на Dimension-DI занял пару дней.

Сначала все файлы конфигурации разобрали и перевели в формат конфигурации Dimension-DI (с помощью LLM):

Конфигурация Dagger 2
package ru.dimension.ui.config;

import dagger.Binds;
import dagger.Module;
import javax.inject.Named;
import ru.dimension.ui.cache.AppCache;
import ru.dimension.ui.cache.impl.AppCacheImpl;

@Module
public abstract class CacheConfig {

  @Binds
  @Named("appCache")
  public abstract AppCache bindAppCache(AppCacheImpl appCache);
}
package ru.dimension.ui.config;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dagger.Module;
import dagger.Provides;
import java.nio.file.Paths;
import javax.inject.Singleton;
import ru.dimension.ui.helper.FilesHelper;
import ru.dimension.ui.helper.ReportHelper;

@Module
public class FileConfig {

  @Provides
  @Singleton
  public FilesHelper getFilesHelper() {
    return new FilesHelper(Paths.get(".").toAbsolutePath().normalize().toString());
  }

  @Provides
  @Singleton
  public Gson getGson() {
    return new GsonBuilder()
        .setPrettyPrinting()
        .create();
  }

  @Provides
  @Singleton
  public ReportHelper getReportHelper() {
    return new ReportHelper();
  }
}
package ru.dimension.ui.config;

import dagger.Binds;
import dagger.Module;
import javax.inject.Named;
import ru.dimension.ui.state.NavigatorState;
import ru.dimension.ui.state.SqlQueryState;
import ru.dimension.ui.state.impl.NavigatorStateImpl;
import ru.dimension.ui.state.impl.SqlQueryStateImpl;

@Module
public abstract class StateConfig {

  @Binds
  @Named("navigatorState")
  public abstract NavigatorState bindNavigatorState(NavigatorStateImpl navigatorState);

  @Binds
  @Named("sqlQueryState")
  public abstract SqlQueryState bindSqlQueryState(SqlQueryStateImpl sqlQueryState);
}
Конфигурация Dimension-DI
package ru.dimension.ui.config.core;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.nio.file.Paths;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import ru.dimension.db.core.DStore;
import ru.dimension.di.DimensionDI;
import ru.dimension.di.ServiceLocator;
import ru.dimension.ui.cache.AppCache;
import ru.dimension.ui.cache.impl.AppCacheImpl;
import ru.dimension.ui.collector.Collector;
import ru.dimension.ui.collector.CollectorImpl;
import ru.dimension.ui.collector.collect.prometheus.ExporterParser;
import ru.dimension.ui.collector.http.HttpResponseFetcher;
import ru.dimension.ui.collector.http.HttpResponseFetcherImpl;
import ru.dimension.ui.helper.ColorHelper;
import ru.dimension.ui.helper.FilesHelper;
import ru.dimension.ui.helper.ReportHelper;
import ru.dimension.ui.manager.ConfigurationManager;
import ru.dimension.ui.warehouse.LocalDB;

public final class CoreConfig {

  private CoreConfig() {
  }

  public static void configure(DimensionDI.Builder builder) {
    builder
        // Cache
        .bindNamed(AppCache.class, "appCache", AppCacheImpl.class)

        // Collector
        .bindNamed(Collector.class, "collector", CollectorImpl.class)

        // Executors
        .provideNamed(ScheduledExecutorService.class, "executorService", ServiceLocator.singleton(
            () -> Executors.newScheduledThreadPool(10)
        ))
        .provideNamed(ru.dimension.ui.executor.TaskExecutorPool.class, "taskExecutorPool",
                      ServiceLocator.singleton(ru.dimension.ui.executor.TaskExecutorPool::new))

        // File/Gson/Report helpers
        .provide(FilesHelper.class, ServiceLocator.singleton(
            () -> new FilesHelper(Paths.get(".").toAbsolutePath().normalize().toString())
        ))
        .provide(Gson.class, ServiceLocator.singleton(
            () -> new GsonBuilder().setPrettyPrinting().create()
        ))
        .provide(ReportHelper.class, ServiceLocator.singleton(ReportHelper::new))

        // Color helper (uses FilesHelper and ConfigurationManager)
        .provide(ColorHelper.class, ServiceLocator.singleton(
            () -> new ColorHelper(
                ServiceLocator.get(FilesHelper.class),
                ServiceLocator.get(ConfigurationManager.class, "configurationManager"))
        ))

        // Local DB
        .bindNamed(DStore.class, "localDB", LocalDB.class)

        // HTTP / Parser
        .provideNamed(ExporterParser.class, "exporterParser", () -> ServiceLocator.get(ExporterParser.class))
        .bindNamed(HttpResponseFetcher.class, "httpResponseFetcher", HttpResponseFetcherImpl.class);
  }
}

После этого поправили точку входа в приложение Application:

Точка входа в Application с Dagger 2
package ru.dimension.ui;

import lombok.extern.log4j.Log4j2;
import ru.dimension.ui.config.MainComponent;
import ru.dimension.ui.laf.LaF;
import ru.dimension.ui.laf.LaFType;
import ru.dimension.ui.prompt.Internationalization;
import ru.dimension.ui.view.BaseFrame;

@Log4j2
public class Application {

  private static MainComponent mainComponent;

  public static MainComponent getInstance() {
    return mainComponent;
  }

  /**
   * Use LaF parameter in VM option to enable dark, light or default theme
   * <p>
   * Supported any of: "-DLaF=dark", "-DLaF=light", "-DLaF=default"
   * <p>
   * Example: java -DLaF=dark -Dfile.encoding=UTF8 -jar desktop-1.0-SNAPSHOT-jar-with-dependencies.jar
   */
  public static void main(String... args) {
    System.getProperties().setProperty("oracle.jdbc.J2EE13Compliant", "true");

    if ("ru".equals(System.getProperty("user.language"))) {
      Internationalization.setLanguage("ru");
    } else if ("en".equals(System.getProperty("user.language"))) {
      Internationalization.setLanguage("en");
    } else {
      Internationalization.setLanguage();
    }

    try {
      System.setProperty("flatlaf.uiScale", "1.1x");

      String lafVMOption = "LaF";
      if (LaFType.DEFAULT.name().equalsIgnoreCase(System.getProperty(lafVMOption))) {
        LaF.setLookAndFeel(LaFType.DEFAULT);
      } else if (LaFType.LIGHT.name().equalsIgnoreCase(System.getProperty(lafVMOption))) {
        LaF.setLookAndFeel(LaFType.LIGHT);
      } else if (LaFType.DARK.name().equalsIgnoreCase(System.getProperty(lafVMOption))) {
        LaF.setLookAndFeel(LaFType.DARK);
      } else {
        LaF.setLookAndFeel(LaFType.DEFAULT);
      }
    } catch (Exception e) {
      log.catching(e);
    }

    mainComponent = ru.dimension.ui.config.DaggerMainComponent.create();

    BaseFrame baseFrame = mainComponent.createBaseFrame();
    baseFrame.setVisible(true);
  }
}
Точка входа в Application c Dimension-DI
package ru.dimension.ui;

import lombok.extern.log4j.Log4j2;
import ru.dimension.di.ServiceLocator;
import ru.dimension.ui.config.DIConfig;
import ru.dimension.ui.laf.LaF;
import ru.dimension.ui.laf.LaFType;
import ru.dimension.ui.prompt.Internationalization;
import ru.dimension.ui.view.BaseFrame;

@Log4j2
public class Application {

  /**
   * Use LaF parameter in VM option to enable dark, light or default theme
   * <p>
   * Supported any of: "-DLaF=dark", "-DLaF=light", "-DLaF=default"
   * <p>
   * Example: java -DLaF=dark -Dfile.encoding=UTF8 -jar desktop-1.0-SNAPSHOT-jar-with-dependencies.jar
   */
  public static void main(String... args) {
    System.getProperties().setProperty("oracle.jdbc.J2EE13Compliant", "true");

    if ("ru".equals(System.getProperty("user.language"))) {
      Internationalization.setLanguage("ru");
    } else if ("en".equals(System.getProperty("user.language"))) {
      Internationalization.setLanguage("en");
    } else {
      Internationalization.setLanguage();
    }

    try {
      System.setProperty("flatlaf.uiScale", "1.1x");

      String lafVMOption = "LaF";
      if (LaFType.DEFAULT.name().equalsIgnoreCase(System.getProperty(lafVMOption))) {
        LaF.setLookAndFeel(LaFType.DEFAULT);
      } else if (LaFType.LIGHT.name().equalsIgnoreCase(System.getProperty(lafVMOption))) {
        LaF.setLookAndFeel(LaFType.LIGHT);
      } else if (LaFType.DARK.name().equalsIgnoreCase(System.getProperty(lafVMOption))) {
        LaF.setLookAndFeel(LaFType.DARK);
      } else {
        LaF.setLookAndFeel(LaFType.DEFAULT);
      }
    } catch (Exception e) {
      log.catching(e);
    }

    DIConfig.init();

    BaseFrame baseFrame = ServiceLocator.get(BaseFrame.class);
    baseFrame.setVisible(true);
  }
}

Затем автозаменой в OpenIDE поменяли javax.inject —> jakarta.inject и начали тестирование работы приложения.

Особых проблем не обнаружилось. Единственное — в нескольких файлах проекта Dimension-DI выявил циклические зависимости и сообщил об этом в консоли при старте — удобно. Как справлялся с ними Dagger 2? Через позднее связывание Lazy. Надо написать автору, чтобы больше так не делал.

Следующий этап — правка тестов. Тут особых проблем тоже не возникло. Особенно порадовало раскомментирование @Disabled на классах тестирующих автоматику по пересозданию графиков при SeriesExceedExceptionи последующее успешное прохождение тестов с первого раза. Отключены тесты были из-за сложностей конфигурации Dagger 2.

Да, внутри тестируемых классов пришлось использовать ServiceLocator напрямую — но: а) это минимальное изменение (одно), которое сейчас покрыто тестами; и б) теперь эти классы можно тестировать в изолированном окружении — удобно, а не так, как это было в Dagger 2.

Кстати, во время тестирования два DI работали совместно не мешая друг другу.

Небольшое сравнение Dimension-DI с другими решениями

Dimension-DI против "Большой тройки"

Функция

Dimension-DI

Spring IoC

Google Guice

Dagger 2

Стандарт аннотаций

JSR-330 (Jakarta)

Spring-specific + JSR-330

JSR-330

JSR-330 + кастомные

Внедрение зависимостей

Только через конструктор

Конструктор, поле, метод

Конструктор, поле, метод

На основе конструктора

Кривая обучения

⭐ Минимальная

⭐⭐⭐⭐⭐ Высокая

⭐⭐⭐ Средняя

⭐⭐⭐ Средняя

Производительность

⭐⭐⭐⭐⭐ Высочайшая

⭐⭐ Низкая

⭐⭐⭐ Средняя

⭐⭐⭐⭐⭐ Высочайшая

Время запуска

Сверхбыстрое

Медленное

Быстрое

Мгновенное (во время компиляции)

Метаданные в runtime

JDK Class-File API

Динамическая рефлексия

Динамическая рефлексия

Нет (во время компиляции)

Генерация байт-кода

Нет

Интенсивное использование прокси

Интенсивное использование прокси

Только во время компиляции

Scope

@Singleton

Request, Session, Singleton, Prototype

Singleton, кастомные

Singleton, кастомные

Поддержка @Singleton

✅ Да

✅ Да

✅ Да

✅ Да

Квалификаторы @Named

✅ Да

✅ Да

✅ Да

✅ Да

Пользовательские провайдеры

✅ provide()

✅ @Bean

✅ @Provides

✅ @Provides

Внедрение в поля

❌ Нет

✅ Да

✅ Да

✅ Да

Внедрение в методы

❌ Нет

✅ Да

✅ Да

✅ Да

Коллекции/Multi-bind

❌ Нет

✅ Да

✅ Да

✅ Yes (@IntoSet/@IntoMap/…)

Обнаружение циклических зависимостей

✅ Да, явная ошибка

✅ Да

✅ Да

✅ Во время компиляции

Система модулей/конфигурации

Fluent Builder

@Configuration + XML

Классы Module

Интерфейс Component

Поддержка тестирования

✅ Override, Clear

✅ Профили, моки

✅ Переопределение привязок

✅ Тестовые компоненты

Сканирование JAR/директорий

✅ И то, и другое

✅ И то, и другое

Только вручную

Н/Д (во время компиляции)

Размер фреймворка

~19 КБ

~30 МБ+

~782 КБ

~47 КБ

Лучше всего подходит для

Микросервисы, утилиты, минимальные накладные расходы

Корпоративные приложения, полный веб-стек

Средние проекты, модульность

Android, безопасность на этапе компиляции

Нулевая конфигурация

✅ Полное сканирование classpath

⚠️ Требует настройки

Ручная регистрация

Настройка во время компиляции

Dimension-DI против альтернативных легковесных контейнеров

Функция

Dimension-DI

PicoContainer

HK2

Avaje Inject

Стандарт аннотаций

JSR-330

Только кастомные

JSR-330

JSR-330

Легковесность

✅ Сверхлегкий

✅ Очень легкий

⚠️ Умеренный

✅ Легкий

Сканирование Classpath

✅ Class-File API

❌ Только вручную

✅ Да

✅ Да

Внедрение через конструктор

✅ Только метод

✅ Да

✅ Да

✅ Да

Внедрение в поля

❌ Нет

✅ Да

✅ Да

✅ Да

Scope

@Singleton

Singleton

Singleton, request, кастомные

Singleton, кастомные

Квалификаторы @Named

✅ Да

❌ Нет

✅ Да

✅ Да

Пользовательские провайдеры

✅ provide()

✅ Ручные фабрики

✅ @Factory

✅ @Factory

Обнаружение циклических зависимостей

✅ Явная ошибка

❌ Ошибка времени выполнения

✅ Да

✅ Да

Производительность

⭐⭐⭐⭐⭐

⭐⭐⭐⭐

⭐⭐⭐

⭐⭐⭐⭐⭐

Время запуска

Сверхбыстрое

Очень быстрое

Быстрое

Быстрейшее (во время компиляции)

Рефлексия в runtime

Минимальная

Интенсивная

Умеренная

Нет (во время компиляции)

Паттерн Service Locator

✅ Только внутренне

✅ Основная модель

✅ HK2ServiceLocator

✅ Только внутренне

Модель компиляции

Сканирование в runtime

Ручная регистрация

Сканирование в runtime

Во время компиляции (APT)

Интеграция с Maven

✅ Простая

✅ Простая

✅ Простая (Jersey)

✅ Простая (APT)

Поддержка тестирования

✅ Override, Clear

✅ Rebind (перепривязка)

✅ Да

✅ Да

Размер фреймворка

~19 КБ

~327 КБ

~131 КБ

~80 КБ

Активная разработка

✅ Современная

⚠️ Неактивна

✅ Активная

✅ Активная

Готовность к Jakarta Inject

✅ Полная

⚠️ Частичная

✅ Да

✅ Да

Лучше всего подходит для

Микросервисы, быстрый запуск

Встраиваемые, кастомные, устаревшие системы

OSGi, модульные системы

DI с проверкой на этапе компиляции, GraalVM

Версия Java

25+

8+

8+

11+

Выводы

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

С другой стороны, runtime-ге��ерация практически во всех DI – это Java Reflection API, что снижает быстродействие (особенно на больших объемах объектов в графе) и требует ресурсов на сопровождение всей этой инфраструктуры. Для небольших и среднего размера проектов – это очевидный overhead.

Dimension-DI исключает рефлексию на этапе discovery (Class-File API) и использует MethodHandles при создании объектов. То есть это runtime-DI без java.lang.reflect на «горячем пути» инстанцирования — в моих сценариях это быстро. Посмотрим, что из этого выйдет – на будущее, надо бы добавить бенчмарки и кэш графа для реальных метрик.

Ссылки и дополнительные материалы

Вроде все, спасибо за внимание!

Комментарии (2)


  1. ant1free2e
    01.11.2025 07:31

    А чего не поддержали внедрение через сеттер? Через конструктор же как раз для тестирования не очень удобно


    1. akardapolov Автор
      01.11.2025 07:31

      А чего не поддержали внедрение через сеттер?

      Можно сделать, думаю не проблема. Как вариант через ServiceLocator устанавливать значение, но это не очень правильно. На меня сильно повлияли аргументы команды Spring на предпочтение использования Constructor-Based Injection.

      Через конструктор же как раз для тестирования не очень удобно

      Все решаемо, просто получаем готовый объект через ServiceLocator в тестах.