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.
| разные интерфейсы являются разными ключами. Если одна реализация поддерживает широкий и узкий интерфейсы, каждый публично запрашиваемый интерфейс регистрируется отдельно. Получение двух интерфейсов может создать два экземпляра фасада в одном контексте. |
Регистрация в ядре
Для новой зависимости ядра:
-
Создайте узкий интерфейс
RepositoryилиService. -
Разместите реализацию по правилам архитектуры (
repository,impl,usecaseи другие проверяемые пакеты). -
Добавьте привязку интерфейса в
RepositoryModule.create()илиServiceModule.create(). -
Передайте зависимости через конструктор; repositories получайте из переданного
RepositoryProvider. -
Добавьте unit-тесты поведения и тест разрешения интерфейса через фабрику/
ServerContext, если меняется DI-граф. -
Обновите документацию публичного контракта, транзакций, событий и ошибок.
Не создавайте отдельный 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 произвольных объектов, не обнаруживает циклы и не проверяет полный граф при старте. Ошибка отсутствующей привязки проявляется при первом запросе соответствующего интерфейса.