Google Guice 运行时依赖注入

Google Guice runtime dependency injection

我正在寻找一种在运行时使用 google guice 动态 select 正确依赖关系的方法。

我的用例是一个 kotlin 应用程序,它可以根据提供的配置文件使用 sqlite 或 h2 数据库。

执行应用程序时读取该文件,如果未找到数据库,则会创建正确的数据库并迁移到其中。

我的数据库结构包含 Database(接口)、H2Database: DatabaseSQLiteDatabase: Database 和模块绑定 class,如下所示:

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

到目前为止,单独使用 SQlite,我会简单地请求依赖使用:

@Inject 
@SQLite
private lateinit var database: Database

如何在运行时制作此 selection?

在不太了解您的代码具体情况的情况下,我将提供三种通用方法。

(此外,我从未使用过 Kotlin。我希望 Java 个示例足以让您解决问题。)


第一种方法

听起来您需要一些重要的逻辑来确定哪个数据库实现是正确的。这是 ProviderBinding. Instead binding Database to a specific implementation, you bind Database to a class that is responsible providing instances (a Provider 的 classic 案例。例如,您可能有此 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.");
        }
    }
}

(旁注:此示例代码每次都会为您提供一个新实例。将其也 return 设为单例实例相当简单。)

然后,要使用它,您有两种选择。在您的模块中,您不会将 Database 绑定到特定的实现,而是绑定到您的 DatabaseProvider。像这样:

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

这种方法的优点是,在 Guice 尝试构造一个需要 Database 作为其构造函数参数之一的对象之前,您不需要知道正确的数据库实现。


第二种方法

您可以创建一个 DatabaseRoutingProxy class 实现 Database,然后委托给正确的数据库实现。 (我专业地使用过这种模式。我认为这种设计模式没有 "official" 名称,但您可以找到针对每种绑定类型的讨论 here.) This approach is based on lazy loading with Provider using the Providers that Guice automatically creates(1)。

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
}

并且在您的 Guice 模块中:

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);
}

这种方法的优点是,在实际进行数据库调用之前,您不需要知道要使用哪个数据库实现。

这两种方法都假设您不能实例化 H2Database 或 SqliteDatabase 的实例,除非支持数据库文件实际存在。如果可以在没有支持数据库文件的情况下实例化对象,那么您的代码就会变得更加简单。 (只要有一个 router/proxy/delegator/whatever 将实际的 Database 实例作为构造函数参数。)


第三种方法

这种方法与前两种方法完全不同。在我看来,您的代码实际上是在处理两个问题:

  1. 数据库真的存在吗? (如果没有,那就做一个。)
  2. 存在哪个数据库? (并获得正确的 class 与之交互。)

如果您甚至可以在创建需要知道问题 2 答案的 guice 注入器之前解决问题 1,那么您就不需要做任何复杂的事情。您可以只拥有这样的数据库模块:

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);
        }
    }
}

由于您已经将问题 1 和问题 2 分开,当您创建将使用 MyDatabaseModule 的注入器时,您可以为构造函数参数传入适当的值。


备注

  1. Injector 文档指出每个绑定 T 都会存在一个 Provider<T>。我已经成功创建了绑定,但没有创建相应的提供者,因此 Guice 必须自动为配置的绑定创建一个提供者。 (编辑:我发现 more documentation 更清楚地说明了这一点。)