Image

Сравнение Dagger 2 и runtime DI: как мигрировать на JSR-330 и Class-File API

9d24d6516437469f556bce1250aa1677

Когда начинал разработку системы многомерного анализа данных временных рядов 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 2package 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-DIpackage 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 2package 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-DIpackage 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 на «горячем пути» инстанцирования — в моих сценариях это быстро. Посмотрим, что из этого выйдет – на будущее, надо бы добавить бенчмарки и кэш графа для реальных метрик.

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

  • 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

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

Источник: habr.com

✅ Найденные теги: новости, Сравнение

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых