Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google Guice runtime dependency injection

Tags:

kotlin

guice

I am looking for a way to dynamically select the correct dependency during runtime using google guice.

My usecase is a kotlin application which can work with either sqlite or h2 databases depending on the configuration file provided.

The file is read when the application is executed and if the database is not found, the correct one is created and migrated into.

My database structure contains the Database (Interface), H2Database: Database, SQLiteDatabase: Database and the module binding class which looks like this:

class DatabaseModule: KotlinModule() {
    override fun configure() {
        bind<Database>().annotatedWith<configuration.H2>().to<H2Database>()
        bind<Database>().annotatedWith<configuration.SQLite>().to<SQLiteDatabase>()
    }
}

So far, with SQlite alone, I would simply request the dependency using:

@Inject 
@SQLite
private lateinit var database: Database

How would I make this selection during runtime?

like image 231
sandroydecli Avatar asked Mar 05 '23 13:03

sandroydecli


1 Answers

Without knowing too much about the specific of your code, I'll offer three general approaches.

(Also, I have never used Kotlin. I hope Java samples are enough for you to figure things out.)


First Approach

It sounds like you need some non-trivial logic to determine which Database implementation is the right one to use. This is a classic case for a ProviderBinding. Instead binding Database to a specific implementation, you bind Database to a class that is responsible providing instances (a Provider). For example, you might have this class:

public class MyDatabaseProvider.class implements Provider<Database> {

    @Inject
    public MyDatabaseProvider.class(Provider<SQLiteDatabase> sqliteProvider, Provider<H2Database> h2Provider) {
        this.sqliteProvider = sqliteProvider;
        this.h2Provider = h2Provider;
    }

    public Database get() {
        // Logic to determine database type goes here
        if (isUsingSqlite) {
            return sqliteProvider.get();
        } else if (isUsingH2) {
            return h2Provider.get();
        } else {
            throw new ProvisionException("Could not determine correct database implementation.");
        }
    }
}

(Side note: This sample code gets you a new instance every time. It is fairly straightforward to make this also return a singleton instance.)

Then, to use it, you have two options. In your module, you would bind Database not to a specific implementation, but to your DatabaseProvider. Like this:

protected void configure() {
    bind(Database.class).toProvider(MyDatabaseProvider.class);
}

The advantage of this approach is that you don't need to know the correct database implementation until Guice tries to construct an object that requires Database as one of its constructor args.


Second Approach

You could create a DatabaseRoutingProxy class which implements Database and then delegates to the correct database implementation. (I've used this pattern professionally. I don't think there's an "official" name for this design pattern, but you can find a discussion here.) This approach is based on lazy loading with Provider using the Providers that Guice automatically creates(1) for every bound type.

public class DatabaseRoutingProxy implements Database {
    private Provider<SqliteDatabse> sqliteDatabaseProvider;
    private Provider<H2Database> h2DatabaseProvider;

    @Inject
    public DatabaseRoutingProxy(Provider<SqliteDatabse> sqliteDatabaseProvider, Provider<H2Database> h2DatabaseProvider) {
        this.sqliteDatabaseProvider = sqliteDatabaseProvider;
        this.h2DatabaseProvider = h2DatabaseProvider;
    }

    // Not an overriden method
    private Database getDatabase() {
        boolean isSqlite = // ... decision logic, or maintain a decision state somewhere

        // If these providers don't return singletons, then you should probably write some code 
        // to call the provider once and save the result for future use.
        if (isSqlite) {
            return sqliteDatabaseProvider.get();
        } else {
            return h2DatabaseProvider.get();
        }
    }

    @Override
    public QueryResult queryDatabase(QueryInput queryInput) {
        return getDatabase().queryDatabase(queryInput);
    }

    // Implement rest of methods here, delegating as above
}

And in your Guice module:

protected void configure() {
    bind(Database.class).to(DatabaseRoutingProxy.class);
    // Bind these just so that Guice knows about them. (This might not actually be necessary.)
    bind(SqliteDatabase.class);
    bind(H2Database.class);
}

The advantage of this approach is that you don't need to be able to know which database implementation to use until you actually make a database call.

Both of these approaches have been assuming that you cannot instantiate an instance of H2Database or SqliteDatabase unless the backing database file actually exists. If it's possible to instantiate the object without the backing database file, then your code becomes much simpler. (Just have a router/proxy/delegator/whatever that takes the actual Database instances as the constructor args.)


Third Approach

This approach is completely different then the last two. It seems to me like your code is actually dealing with two questions:

  1. Does a database actually exist? (If not, then make one.)
  2. Which database exists? (And get the correct class to interact with it.)

If you can solve question 1 before even creating the guice injector that needs to know the answer to question 2, then you don't need to do anything complicated. You can just have a database module like this:

public class MyDatabaseModule extends AbstractModule {

    public enum DatabaseType {
        SQLITE,
        H2
    }

    private DatabaseType databaseType;

    public MyDatabaseModule(DatabaseType databaseType) {
        this.databaseType = databaseType;
    }

    protected void configure() {
        if (SQLITE.equals(databaseType)) {
            bind(Database.class).to(SqliteDatabase.class);
        } else if (H2.equals(databaseType)) {
            bind(Database.class).to(H2Database.class);
        }
    }
}

Since you've separated out questions 1 & 2, when you create the injector that will use the MyDatabaseModule, you can pass in the appropriate value for the constructor argument.


Notes

  1. The Injector documentation states that there will exist a Provider<T> for every binding T. I have successfully created bindings without creating the corresponding provider, therefore Guice must be automatically creating a Provider for configured bindings. (Edit: I found more documentation that states this more clearly.)
like image 154
Matthew Pope Avatar answered Mar 11 '23 12:03

Matthew Pope