Guice、Settings和样板代码

4

我有一个应用程序,使用Guice并从配置文件中读取一些配置设置。我像这样加载它:

@Provides @Singleton
Settings provideSettings() {
    // code that loads the settings
}

某些对象需要特定的设置,而其他对象需要其他的设置。对我来说,在构造函数中传递这些东西是有意义的,但这样我最终会得到很多样板代码,比如:
@Provides @Named("integerValueSetting1")
int provideIntegerValueSetting1(Settings settings) {
    return settings.getInteger("integerValueSetting1");
}

我需要为每种设置类型都创建一个@Provides方法,还要在构造函数中注释适当的设置。就像这样:

@Inject
public MyClass(@Assisted String objectName, @Named("integerValueSetting1") myValue) {
    // blah blah constructor
}

这似乎并没有给我带来太多帮助!如果为每个设置参数创建自定义注释类,情况会更糟。肯定有更好的方法,对吧?

一种解决方案可能是直接传递Settings对象,但这违反了最佳实践:仅注入直接依赖项...


1
Settings是一个直接依赖项。即使您不调用Settings对象的每个方法,也不能说明它不是直接依赖项。 - JB Nizet
@JBNizet 嗯,我确实有一个与设置对象相关联的接口...现在它只是从一个平面文件中读取,但我想以后将其变成一个数据库。但我想无论哪种方式,它都会有相同的接口... - durron597
1
因此,直接注入设置。为了进行单元测试,您只需注入一个模拟的设置对象并存根被测试对象使用的方法。这是依赖注入的经典用法。 - JB Nizet
2个回答

2
如果你有很多设置,不想为每个设置创建新的绑定注解,那么可以尝试将它们放在枚举中,并在通用绑定注解中使用该枚举。这可能是一个比较复杂的解决方案,但也可以节省你试图避免的样板代码。
通过这种方式,你可以匹配对象引用(便于IDE),而不是字符串(慢且脆弱),并且仍然只创建一个绑定注解。
public enum Config {
  DB_NAME("db_name"),
  DB_HOST("db_host_name_specified_in_file"),
  SOME_NUMBER("some_number"),
  ;

  private final String propertyName;

  private Config(String propertyName) {
    this.propertyName = propertyName;
  }

  public String getPropertyName() {
    return propertyName;
  }

  public InjectConfig annotation() {
    // Create an implementation of InjectConfig for ease of binding.
    return new InjectConfig() {
      @Override public Class<? extends Annotation> annotationType() {
        return InjectConfig.class;
      }

      @Override public Config value() {
        return Config.this;
      }

      @Override public boolean equals(Object obj) {
        if (obj == this) {
          return true;
        } else if (!(obj instanceof InjectConfig)) {
          return false;
        }
        return value() == ((InjectConfig) obj).value();
      }

      /** @see Annotation#hashCode */
      @Override public int hashCode() {
        return (127 * "value".hashCode()) ^ value().hashCode();
      }
    };
  }

  @Retention(RetentionPolicy.RUNTIME)
  @BindingAnnotation
  public static @interface InjectConfig {
    Config value();
  }
}

现在您可以通过循环迭代每个并绑定它们:
public class YourModule extend AbstractModule {
  @Override public void configure() {
    // You can get a Provider in a Module as long as you
    // don't call get() before the injector exists.
    Provider<Settings> settingsProvider = binder().getProvider(Settings.class);
    for (Config config : Config.values()) {
      String propertyName = config.getPropertyName();
      // Guice's TypeConverter will convert Strings to the right type.
      bind(String.class).annotatedWith(config.annotation()).toProvider(
          new GetValueFromSettingsProvider(settingsProvider, propertyName));
    }
  }
}

直接注入你所需的内容:

/** Your constructor */
YourClass(@InjectConfig(DB_USER) String user,
    @InjectConfig(SOME_NUMBER) int number) { }

我没有测试过这个,但据我所知它应该可以工作。考虑到您的特定设置用例,您可能需要调整您编写的GetValueFromSettingsProvider方法,或在枚举中编写可重写的getConfigValueFromSettings方法。请记住,无论如何,您仍然需要存储(枚举键,文件中的属性名称,属性类型)三元组,而枚举似乎是以编程方式管理它的最佳方法。


这太棒了。我已经有了枚举,只是没有用它来做注释。 - durron597
我还没有尝试过,但是使用注释会不会是@Config.InjectConfig?此外,我不确定您是否可以像那样匿名声明InjectConfig - durron597
这是为了阅读方便而嵌套的SO; Config.InjectConfig有效,或者将其分解(在IDE中可能更好)。匿名实现确实有效,但我认为最好还是将其分解。 - Jeff Bowman
如果我在我的设置类中使用@Provides而不是Provider,这个会起作用吗? - durron597
Guice在提供程序方面很不错...您可以绑定XProvider<X>(包括使用@Provides创建的隐式提供程序),并请求注入XProvider<X>,任何组合都可以。 但是,您仍然需要一些实现GetValueFromSettingsProvider或类似内容,因为Guice需要能够从您的“设置”中获取绑定值。 - Jeff Bowman

0

请查看Tadeon项目,特别是它的配置功能。

使用以下.properties文件:

foo=foo
list=1, 2, 3

以及组件:

public class PropertyInjectedComponent {

    private final String foo;
    private final List<String> list;

    @Inject
    public PropertyInjectedComponent(
        @Named("foo") String foo,
        @Named("list") List<String> list) {
    this.foo = foo;
    this.list = list;
    }
    ...
}

你可以简单地配置 Guice 模块来扫描命名值的属性:

Injector injector = Guice.createInjector(new AbstractModule() {
        protected void configure() {
            GuiceConfigurations.bindProperties(binder(), new File("src/test/data"), "conf1.properties");
        }
    });

这里有test个文件。

你可以用Apache的Configuration,JDK的Properties或者只是Map<String, String>,或者调查Tadeon源代码来替换你的Settings类,以创建类似的解决方案。


就我个人而言,我的Settings类是由JDK属性支持的,但我还有其他功能(缓存、多种类型的获取方法等)。此外,重要的是我留给自己的选择是可以更改它的后备方式,因为我知道在开发周期的后期,我计划切换到数据库。 - durron597
如果您不想承诺使用额外的库,您可以使用内置的 Names.bindProperties 来实现大部分功能。但是,Tadeon可能具有一些您会喜欢的附加功能。 - Jeff Bowman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接