DI: RepositoryModule и ServiceModule

DI-контейнер bgcrm — небольшой внутренний реестр фабрик. Он не выполняет сканирование classpath и не выбирает реализацию по аннотации: каждый интерфейс repository или сервиса должен быть зарегистрирован явно.

RepositoryModule / plugin DI
            |
            v
    RepositoryFactory -----> RepositoryProvider (один на ServerContext)
                                      |
                                      v
                               repository instance

ServiceModule / plugin DI
            |
            v
      ServiceFactory ------> ServiceProvider (один на ServerContext)
                                      |
                                      v
                         implementation + service proxy

Глобальные фабрики принадлежат GlobalContext. Providers и созданные ими объекты принадлежат конкретному ServerContext.

RepositoryModule

ru.bgcrm.context.core.RepositoryModule создаёт RepositoryFactory ядра. Ключом является интерфейс, наследующий ru.bgcrm.context.core.repository.Repository, а значением — функция создания по общему ConnectionManager контекста.

RepositoryFactory(
    mapOf(
        ExampleRepository::class.java to { manager ->
            DefaultExampleRepository(manager)
        }
    )
)

RepositoryProvider лениво создаёт repository при первом get и кэширует его по точному классу интерфейса до закрытия контекста. Все repositories этого контекста получают один ConnectionManager и поэтому участвуют в общих транзакционных границах.

val repository = repositories.get<ExampleRepository>()

Если привязки нет, фабрика выбрасывает IllegalArgumentException с сообщением Unknown repository binding.

Repository отвечает за хранение данных. SQL должен находиться только в repository/DAO-слое; для ERP-repository используйте ErpRepository, который получает master и slave через ConnectionManager.

ServiceModule

ru.bgcrm.context.core.ServiceModule создаёт ServiceFactory ядра. Ключом является интерфейс, наследующий ru.bgcrm.context.core.service.Service, а фабрика реализации получает текущего пользователя и RepositoryProvider.

ServiceFactory(
    mapOf(
        ExampleService::class.java to { user, repositories ->
            DefaultExampleService(user, repositories.get())
        }
    )
)

ServiceProvider лениво создаёт сервис при первом getService и кэширует его по запрошенному интерфейсу. Наружу возвращается Java proxy, а не сама реализация. Proxy добавляет обработчики вызова и управляет commit/recycle в нетранзакционном контексте.

Если привязки нет, фабрика выбрасывает IllegalArgumentException с сообщением Unknown service binding.

разные интерфейсы являются разными ключами. Если одна реализация поддерживает широкий и узкий интерфейсы, каждый публично запрашиваемый интерфейс регистрируется отдельно. Получение двух интерфейсов может создать два экземпляра фасада в одном контексте.

Регистрация в ядре

Для новой зависимости ядра:

  1. Создайте узкий интерфейс Repository или Service.

  2. Разместите реализацию по правилам архитектуры (repository, impl, usecase и другие проверяемые пакеты).

  3. Добавьте привязку интерфейса в RepositoryModule.create() или ServiceModule.create().

  4. Передайте зависимости через конструктор; repositories получайте из переданного RepositoryProvider.

  5. Добавьте unit-тесты поведения и тест разрешения интерфейса через фабрику/ServerContext, если меняется DI-граф.

  6. Обновите документацию публичного контракта, транзакций, событий и ошибок.

Не создавайте отдельный ConnectionManager внутри repository или сервиса и не выполняйте service lookup в статическом инициализаторе.

Регистрация в плагине

Plugin предоставляет общий GlobalContext.diContainer. Плагин регистрирует зависимости при загрузке:

class DI(private val container: DiContainer) {
    fun init() {
        container.registerRepository(ExampleRepository::class.java) { manager ->
            DefaultExampleRepository(manager)
        }
        container.registerService(ExampleService::class.java) { user, repositories ->
            DefaultExampleService(user, repositories.get())
        }
    }
}

Для большого плагина удобно вынести списки привязок в собственные RepositoryModule и ServiceModule, как сделано в BPMN-плагине, а из DI.init() вызвать их load().

Регистрацию выполняйте на старте плагина, до обработки прикладных запросов. Фабрики изменяемы, но их изменение во время работы не является штатным сценарием: уже разрешённые зависимости остаются в кэше существующего ServerContext.

Перезапись привязок

registerRepository и registerService записывают значение в map по классу интерфейса. Повторная регистрация молча заменяет прежнюю фабрику. Проверки дубликата и приоритета нет.

Следствия:

  • не регистрируйте один интерфейс в нескольких модулях без намеренного замещения;

  • не полагайтесь на порядок загрузки плагинов как на механизм выбора реализации;

  • при замещении добавьте отдельный интеграционный тест и явно опишите причину;

  • изменение привязки не заменяет объект, уже закэшированный provider существующего контекста.

Границы DI

Контейнер внедряет только два вида зависимостей:

  • repository создаётся из ConnectionManager;

  • сервис создаётся из User и RepositoryProvider.

Другие зависимости собираются обычным Kotlin/Java-кодом внутри фабрики. Контейнер не управляет close произвольных объектов, не обнаруживает циклы и не проверяет полный граф при старте. Ошибка отсутствующей привязки проявляется при первом запросе соответствующего интерфейса.