Как связать динамическое связывание @Named

Я хотел бы создать настраиваемый модуль, который будет связывать несколько разных вещей @Named. Приложения / инъекции, которые используют модуль, будут знать @Name заранее, но сам модуль не будет знать, пока не будет создан его экземпляр во время выполнения.

Я использую kotlin в моем примере кода, но рад за ответы на Java.

Это не компилируется, потому что все аннотации @Named должны ссылаться на константные строки, а не на переменные времени выполнения (An annotation argument must be a compile-time constant):

class DbModule(val configPath: String) : KotlinModule() {
    @Provides
    @Named(configPath) // <-- can't do this
    fun provideDbConfig(loader: ConfigLoader): DbConfig { 
         // note ConfigLoader is separately bound, 
         // but a needed depenency of DbConfig
         return DbConfig(loader, configPath)
    }

    @Provides
    @Named(configPath) // <-- can't do this
    fun provideDataSource(
        @Named(configPath)  // <-- can't do this
        dbConfig: DbConfig): DataSource  
    {
        return dbConfig.dataSource
    }
}

Я могу заставить работать привязку DbConfig, добавив провайдера:

private class ConfigProvider
@Inject constructor(
    val loader: ConfigLoader,
    @Named("configPath") val configPath: String
) : Provider<DbConfig> {
    override fun get(): DbConfig {
        return DbConfig(loader, configPath)
    }
}

class DbModule(val configPath: String) : KotlinModule() {
    override configure() {
        bindConstant().annotatedWith(Names.named("configPath"))
            .to(configPath)
        bind<DbConfig>().annotatedWith(Names.named(configPath))
            .toProvider(ConfigProvider::class.java)

    }
}

Но я не уверен, как получить Provider<DataSource> это было бы правильно configPath аннотированный DbConfig() доступно для того, чтобы он мог получить DataSource из конфига? Я мог бы иметь DataSourceProvider который строит свой собственный DbConfig(configPath) так же, как ConfigProvider делает, но кажется предпочтительным, чтобы guice создавал dbconfig через ConfigProvider и быть в состоянии использовать это в DataSourceProvider?

В конце этого я хотел бы иметь возможность вводить следующее:

class BusinessObject1
    @Inject constructor(
        @Named("secondaryDb") val dbConfig: DbConfig
    )
class BusinessObject2
    @Inject constructor(
        @Named("secondaryDb") val dataSource: DataSource
    )

Предполагая, что эти объекты созданы инжектором:

Guice.createInjector(DbModule("secondaryDb"))

(также обратите внимание, что приведенный выше код не позволит создавать оба DbModule("secondaryDb") а также DbModule("tertiaryDb"), но это можно решить с помощью частных модулей, которые я оставил, чтобы избежать дополнительной сложности)

1 ответ

Решение

Вы оставили в стороне PrivateModule, но это именно то, что я бы использовал, чтобы решить вашу проблему. Если я правильно угадал ваш источник KotlinModule, у него есть аналог в KotlinPrivateModule.

Документы Guice отстаивают это решение как "проблему ног робота" (представьте, как связать левую и правую ноги с одинаковыми бедрами, коленями и голенями, но разными левыми и правыми ногами) в своем FAQ в виде вопроса "Как построить две одинаковые, но немного разные деревья объектов?",

В Java это будет выглядеть так:

public class DbModule extends PrivateModule {
  private final String configPath;

  public DbModule(String configPath) { this.configPath = configPath; }

  // (no @Named annotation; bind it like it's the only one!)
  @Provides DbConfig provideDbConfig(ConfigLoader loader) { 
    return new DbConfig(loader, configPath);
  }

  // (no @Named annotation; bind it like it's the only one!)
  @Provides DataSource provideDataSource(DbConfig dbConfig) {
    return dbConfig.dataSource;
  }

  @Override public void configure() {
    // now bind the unqualified one to the qualified one
    bind(DbConfig.class).annotatedWith(Names.named(configPath)).to(DbConfig.class);
    bind(DataSource.class).annotatedWith(Names.named(configPath)).to(DataSource.class);

    // and now you can expose only the qualified ones
    expose(DbConfig.class).annotatedWith(Names.named(configPath));
    expose(DataSource.class).annotatedWith(Names.named(configPath));
  }
}

Таким образом, ваш @Provides Методы не должны пытаться использовать аннотацию, которая доступна только во время выполнения, и вы не загромождаете глобальный Инжектор безусловным связыванием DbConfig и DataSource. Более того - и это реальная выгода для решения - в DbModule вы можете вводить DbConfig и DataSource напрямую, без @Named аннотаций. Это значительно упрощает производство и потребление машин многократного использования, так как у ваших многоразовых деталей не будет @Named аннотации для беспокойства. Вы даже можете связать свой путь конфигурации в виде строки (@Named("configPath") String или же @ConfigPath String) и сделать это непосредственно в DbConfig, что позволит вам пометить DbConfig с помощью @Inject и избавиться от его @Provides метод.

(Для чего это стоит, если вы пошли с альтернативным решением, которое не использует PrivateModules, а вместо этого использовали более длинные и более сложные bind заявления с Names.named, затем DbModule("secondaryDb") а также DbModule("tertiaryDb") будет прекрасно сосуществовать, пока публичные привязки не конфликтуют друг с другом.)

Другие вопросы по тегам