First off - I am only a few days into guice, so I apologize if my questions
are dumb - I have tried to compensate with what I hope are clear examples.
I am attempting to make liberal use of OptionalBinder in my code to allow
some default values to be specified and then potentially overriden by
configuration. I have run into a cascading series of issues. First, lets
start with what works in a simple case that currently works (but does not
have the default value functionality I hope to achieve with OptionalBinder):
public class Simple {
private final int simpleInt;
private final String simpleStr;
@Inject
public Simple(@Named("simpleInt") int simpleInt, @Named("simpleStr")
String simpleStr) {
this.simpleInt = simpleInt;
this.simpleStr = simpleStr;
}
public int getSimpleInt() {
return simpleInt;
}
public String getSimpleStr() {
return simpleStr;
}
}
If I wish to wire up the value of simpleVal from configuration, I can use
Names.bindProperties (which was recommended in the FAQ)
public class SimpleTest {
@Test
public void testGetSimpleVal() throws Exception {
Injector inj = Guice.createInjector(getTestModule());
Simple s = inj.getInstance(Simple.class);
assertEquals(1, s.getSimpleInt());
assertEquals("default", s.getSimpleStr());
}
private Module getTestModule() {
return new AbstractModule() {
@Override protected void configure() {
bind(Simple.class);
Properties p = new Properties();
p.setProperty("simpleInt", "1");
p.setProperty("simpleStr", "default");
Names.bindProperties(binder(), p);
}
};
}
}
So far, so good - Names.bindProperties is great, it even converts my
strings to ints ("1" -> 1 for simpleInt)
Now I attempt to set up default values for simpleInt and simpleStr using
OptionalBinder. I do this by returning another Module with default
bindings. (I assume I should do this on a package not class level, but it
keeps the example simple).
public class Simple {
private final int simpleInt;
private final String simpleStr;
@Inject
public Simple(@Named("simpleInt") int simpleInt, @Named("simpleStr")
String simpleStr) {
this.simpleInt = simpleInt;
this.simpleStr = simpleStr;
}
public int getSimpleInt() {
return simpleInt;
}
public String getSimpleStr() {
return simpleStr;
}
public static AbstractModule getDefaultsModule() {
return new AbstractModule() {
@Override protected void configure() {
OptionalBinder.newOptionalBinder(binder(),
Key.get(Integer.class, Names.named("simpleInt" )))
.setDefault().toInstance(12345);
//OptionalBinder.newOptionalBinder(binder(),
Key.get(String.class, Names.named("simpleInt" )))
// .setDefault().toInstance("12345");
OptionalBinder.newOptionalBinder(binder(),
Key.get(String.class, Names.named("simpleStr")))
.setDefault().toInstance("Simple class default");
}
};
}
}
public class SimpleTest {
@Test
public void testGetSimpleVal() throws Exception {
Injector inj = Guice.createInjector(Simple.getDefaultsModule(),
getTestModule());
Simple s = inj.getInstance(Simple.class);
assertEquals(1, s.getSimpleInt());
assertEquals("default", s.getSimpleStr());
}
private Module getTestModule() {
return new AbstractModule() {
@Override protected void configure() {
bind(Simple.class);
Properties p = new Properties();
p.setProperty("simpleInt", "1");
p.setProperty("simpleStr", "default");
Names.bindProperties(binder(), p);
}
};
}
}
At this point, I get the following error
1) A binding to java.lang.String annotated with
@com.google.inject.name.Named(value=simpleStr) was already configured at
com.baselayer.device.scratch.Simple$1.configure(Simple.java:42).
at com.baselayer.device.scratch.SimpleTest$1.configure(SimpleTest.java:33)
Looking into the source code for Names.bindProperties, I see this is
because it does not use setBinding, but rather normal non-optional
.toInstance.
binder.bind(Key.get(String.class, new NamedImpl(propertyName))).
toInstance(value);
At this point, I assume there is some good reason that you must use
setBindings, though I will make the suggestion that as a new user of guice,
I would expect a normal non-optional .toInstance to override a .setDefault.
So, shamelessly copying the source code of Names.bindProperties, I created
my own replacement test module as follows:
private Module getTestModule() {
return new AbstractModule() {
@Override protected void configure() {
bind(Simple.class);
Properties p = new Properties();
p.setProperty("simpleInt", "1");
p.setProperty("simpleStr", "default");
//Names.bindProperties(binder(), p);
//The following code replaces Names.bindProperties
Binder skippedBinder = binder().skipSources(Names.class);
for (Enumeration<?> e = p.propertyNames();
e.hasMoreElements(); ) {
String propertyName = (String) e.nextElement();
String value = p.getProperty(propertyName);
OptionalBinder.newOptionalBinder(skippedBinder,
Key.get(String.class,
Names.named(propertyName))).setBinding().toInstance(value);
}
}
};
}
However - unlike Names.bindProperties, the String binding will not stick to
simpleInt. The very useful auto-mutation of String to Int that occurs with
non-optional .toInstance does not appear with .setBinding. Either I get
1) No implementation for java.lang.Integer annotated with
@com.google.inject.name.Named(value=simpleInt) was bound.
while locating java.lang.Integer annotated with
@com.google.inject.name.Named(value=simpleInt)
for parameter 0 at
com.baselayer.device.scratch.Simple.<init>(Simple.java:20)
at com.baselayer.device.scratch.SimpleTest$1.configure(SimpleTest.java:27)
If I do not have any .setDefault bound for simpleInt in
Simple.getDefaultsModule (neither Integer.class or String.class), or I get
Failed tests: testGetSimpleVal(com.baselayer.device.scratch.SimpleTest):
expected:<1> but was:<12345>
If I had bound an Integer in Simple.getDefaultsModule with
OptionalBinder.newOptionalBinder(binder(), Key.get(Integer.class,
Names.named("simpleInt" )))
.setDefault().toInstance(12345);
Finally - If I use
OptionalBinder.newOptionalBinder(binder(), Key.get(String.class,
Names.named("simpleInt" )))
.setDefault().toInstance("12345");
But do NOT overwrite it with setBinding(), then I get the following error
1) No implementation for java.lang.Integer annotated with
@com.google.inject.name.Named(value=simpleInt) was bound.
while locating java.lang.Integer annotated with
@com.google.inject.name.Named(value=simpleInt)
for parameter 0 at
com.baselayer.device.scratch.Simple.<init>(Simple.java:20)
at
com.baselayer.device.scratch.SimpleTest$1.configure(SimpleTest.java:30)
(via modules: com.google.inject.util.Modules$OverrideModule ->
com.baselayer.device.scratch.SimpleTest$1)
It appears that guice will bind a String to an int with the normal,
non-optional .toInstance, but .setDefault .toInstance refuses to bind a
String to an int with the OptionalBinder. (but .setBinding .toInstance will
set a String to an Integer, as long as a separate specific Integer binding
does not already exist).
If .setDefault and .setBinding .toInstance had the same semantics as
normal, non-optional .toInstance, I could make my own Names.bindProperties
and use that everywhere instead (with the intent being that if any default
was already set, I would overwrite it)
However, the only thing I have tried that works is to attempt to bind to
every single instance (ensuring that the exact type has been bound),
something like this for getTestModule()
private Module getTestModule() {
return new AbstractModule() {
@Override protected void configure() {
bind(Simple.class);
Properties p = new Properties();
p.setProperty("simpleInt", "1");
p.setProperty("simpleStr", "default");
//Names.bindProperties(binder(), p);
Binder skippedBinder = binder().skipSources(Names.class);
for (Enumeration<?> e = p.propertyNames();
e.hasMoreElements(); ) {
String propertyName = (String) e.nextElement();
String value = p.getProperty(propertyName);
OptionalBinder.newOptionalBinder(skippedBinder,
Key.get(String.class,
Names.named(propertyName))).setBinding().toInstance(value);
try {
int i = Integer.parseInt(value);
OptionalBinder.newOptionalBinder(skippedBinder,
Key.get(Integer.class,
Names.named(propertyName))).setBinding().toInstance(i);
} catch (NumberFormatException asdf) {
}
}
}
};
}
In order to bind a configuration file's properties to @Named instances, I
need to attempt to convert the String to each possible type if I wish to
load dynamically without some kind of type metadata in the configuration
file as well to know which type I should target the binding to.
Having outlined all this, a few questions
1) Is it feasible for the behaviour of OptionalBinder to allow a
.toInstance to override a .setDefault? If so then things like
Names.bindProperties could (maybe) just work as is. If not, why? (I assume
there is a good reason since .to was explicitly disallowed as an override
for .setDefault in the documentation)
2) What is the intended behaviour of Names.bindProperties. Are strings
supposed to be able to inject onto a Integer? Or only inject if a specific
Integer injection doesn't already exist? (Are users supposed to be able to
inject different String and Integer toInstance values for a Named
annotation?) Is this behaviour I am experiencing via .setDefault something
that hasn't been fully thought through, or is there a ruleset for when some
types are supposed to ovverride other types when a more specific mapping
doesn't already exist for the annotation?
Experimenting with Modules.override it seems that normal
non-optional .toInstance starts behaving similarly if the defaults module
binds both an Integer and a String, and the overriding module just binds a
string (As Names.bindProperties).
3) Are there any downsides to just OptionalBinding any configuration file
property to a variety of common primitive types to ensure that all default
injection parameters gets overridden by configuration?
4) If I just want default values (without the Optional<Foo> support),
should I be using Modules.override instead?
5) Is setDefault working as intended, given that is has different semantics
to .setBinding (.setDefault .toInstance String.class will not inject a
String to an Integer in the absence of a Integer injection, but .setBinding
.toInstance will just like normal non-optional .toInstance)
Thanks for reading if you made it this far in my rocky start to DI in java
:)
Josh
--
You received this message because you are subscribed to the Google Groups
"google-guice" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/google-guice.
To view this discussion on the web visit
https://groups.google.com/d/msgid/google-guice/0c3bb455-d65d-4ed4-bf7d-e84d5f84f7af%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.