- Регистрация
 - 23 Август 2023
 
- Сообщения
 - 3 038
 
- Лучшие ответы
 - 0
 
- Реакции
 - 0
 
- Баллы
 - 51
 
Offline
		
		
	
	Когда начинал разработку системы многомерного анализа данных временных рядов 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
Конфигурация на этапе сборки: Гибкий API Builder используется для настройки DI-контейнера. Этот этап включает сканирование classpath на наличие компонентов, помеченных @Inject, анализ их зависимостей и регистрацию провайдеров (рецептов для создания объектов). Именно здесь проявляется "DI" часть.
Разрешение во время выполнения: Во время выполнения зависимости разрешаются с помощью внутреннего, глобально доступного ServiceLocator. Хотя реализация использует Service Locator, дизайн настроен на написание кода вашего приложения с использованием чистого Внедрения через конструктор (Constructor Injection), отделяя ваши компоненты от самого DI-фреймворка. Можно напрямую вызывать ServiceLocator - но только в отдельных случаях, об этом ниже.
Процесс перехода 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  | ❌ Нет  | ✅ Да  | ✅ Да  | ✅    (@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 на «горячем пути» инстанцирования — в моих сценариях это быстро. Посмотрим, что из этого выйдет – на будущее, надо бы добавить бенчмарки и кэш графа для реальных метрик.
Ссылки и дополнительные материалы
Dimension DI: https://github.com/akardapolov/dimension-di — компактный, быстрый DI-фреймворк для Java с простой конфигурацией и внедрением зависимостей в runtime.
Dimension UI: https://github.com/akardapolov/dimension-ui — десктопное приложение для сбора, хранения, визуализации и анализа данных временных рядов.
Dagger 2: https://dagger.dev/
Guice: https://github.com/google/guice
PicoContainer: https://github.com/picocontainer/picocontainer
HK2: https://github.com/eclipse-ee4j/glassfish-hk2
Avaje Inject: https://github.com/avaje/avaje-inject
Micronaut: https://micronaut.io/
Quarkus: https://quarkus.io/
Class-File API (JEP 484): https://openjdk.org/jeps/484
JSR 330: https://github.com/javax-inject/javax-inject
Вроде все, спасибо за внимание!
				
 (@IntoSet/@IntoMap/…)