Pregunta

I have an application that uses Guice and reads in some configuration settings from a config file. I load it like this:

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

Certain objects need certain settings, other objects need other settings. It seems to me to make sense to pass these things in the constructor, but then I end up with a lot of boilerplate code, like:

@Provides @Named("integerValueSetting1")
int provideIntegerValueSetting1(Settings settings) {
    return settings.getInteger("integerValueSetting1");
}

And I have to make such a @Provides method for every single setting type, plus I have to annotate the appropriate settings in the constructor. Like so:

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

This doesn't seem to save me much! Even worse would be if I created a custom annotation class for each setting parameter. There has got to be a better way, right?

One solution could be to pass the Settings object around directly, but that violates best practices: Inject only direct dependencies...

¿Fue útil?

Solución

If you have a ton of settings, and want to avoid creating a new binding annotation for each of them, you might try putting them in an enum and using that enum in a common binding annotation. This might be a little complex of a solution, but it may also save the boilerplate you're trying to avoid.

With this way, you can match object references (IDE-friendly) instead of Strings (slow and brittle), and still only create one binding annotation.

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

Now you can iterate through and bind each one in a loop:

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

And inject only what you need, directly:

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

I didn't get a chance to test this, but as far as I know it should work. Given your specific settings use-case you may need to massage the GetValueFromSettingsProvider you write, or write an overridable getConfigValueFromSettings method in the enum. Remember, though, that one way or another you're still going to need to store the (enum key, property name in file, property type) tuple, and it seems like an Enum is the best way to manage that programmatically.

Otros consejos

check out Tadeon project, especially its configuration functionality.

with the following .properties file:

foo=foo
list=1, 2, 3

and component:

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

you can simply configure guice module to scan properties for named values:

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

here are test files.

you can either replace your Settings class with apache Configuration, JDK Properties or just Map<String, String> or investigate Tadeon source to create similar solution for your Settings class.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top