I'd like to create a configurable Module that will bind a few different @Named things. Applications/injections that use the module will know the @Name ahead of time, but the module itself won't know until it's instantiated at runtime.
I'm using kotlin in my example code, but happy for java answers.
This fails to compile because all of the @Named annotations need to refer to constant strings, not runtime variables (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
}
}
I can get the DbConfig binding to work by adding a Provider:
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)
}
}
But I'm not sure how to get a Provider<DataSource>
that would have the correct configPath
annotated DbConfig()
available to it so that it can get the DataSource
out of the config? I might be able to have a DataSourceProvider
that constructs its own DbConfig(configPath)
the same way the ConfigProvider
does, but it seems preferable to have guice create the dbconfig via the ConfigProvider
and be able to leverage that in the DataSourceProvider
?
At the end of this, I'd like to be able to have the following injected:
class BusinessObject1
@Inject constructor(
@Named("secondaryDb") val dbConfig: DbConfig
)
class BusinessObject2
@Inject constructor(
@Named("secondaryDb") val dataSource: DataSource
)
Assuming that those objects are created by an injector:
Guice.createInjector(DbModule("secondaryDb"))
(also note the code above won't allow creation of both DbModule("secondaryDb")
and DbModule("tertiaryDb")
, but that can be solved with private modules, which I've left to avoid additional complexity)
You've left aside PrivateModule, but that's exactly what I'd use to solve your problem. If I've guessed your KotlinModule source correctly, it has a counterpart in KotlinPrivateModule.
Guice docs advocate for this solution as the "robot legs problem" (imagine binding left and right legs with identical thighs, knees, and shins, but different left and right legs) in its FAQ as the question "How do I build two similar but slightly different trees of objects?".
In Java this would look like:
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));
}
}
This way your @Provides
methods don't need to try to use an annotation that is only available at runtime, and you don't clutter the global Injector with unqualified DbConfig and DataSource binding. Futhermore—and this is the real benefit to the solution—within DbModule you can inject DbConfig and DataSource directly without @Named
annotations. This makes it much easier to produce and consume reusable machinery, since your reusable pieces won't have any @Named
annotations to worry about. You could even bind your config path as a String (@Named("configPath") String
or @ConfigPath String
) and have that directly injectable into DbConfig, allowing you to mark DbConfig with @Inject
and get rid of its @Provides
method.
(For what it's worth, if you had gone with an alternate solution that doesn't use PrivateModules and instead used longer and more-complex bind
statements with Names.named
, then DbModule("secondaryDb")
and DbModule("tertiaryDb")
would coexist just fine as long as the public bindings don't conflict with one another.)