The following Guice module binds a property file to the @Named
annotation.
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
// Omitted: other imports
public class ExampleModule extends AbstractModule {
@Override
protected void configure() {
Names.bindProperties(binder(), getProperties());
}
private Properties getProperties() {
// Omitted: return the application.properties file
}
}
I can now inject properties directly into my classes.
public class Example {
@Inject
@Named("com.example.title")
private String title;
@Inject
@Named("com.example.panel-height")
private int panelHeight;
}
The values read from a properties file are strings but, as you can see in the example above, Guice is capable of doing type conversion for int
fields.
Now, given the property com.example.background-color=0x333333
I would like to able to get the same type conversion for an arbitrary class, like:
public class Example {
@Inject
@Named("com.example.background-color")
private Color color;
}
Let's say that the Color
class contains a static method decode()
and I can obtain a new Color
instance by calling Color.decode("0x333333")
.
How can I configure Guice to do this automatically and behind the scenes for me?
I found a solution by myself looking into the Guice sources, although I have to say it's not the prettiest (more on this later on).
First of all, we need to create a TypeConverter
.
import com.google.inject.TypeLiteral;
import com.google.inject.spi.TypeConverter;
// Omitted: other imports
public class ColorTypeConverter implements TypeConverter {
@Override
public Object convert(String value, TypeLiteral<?> toType) {
if (!toType.getRawType().isAssignableFrom(Color.class)) {
throw new IllegalArgumentException("Cannot convert type " + toType.getType().getTypeName());
}
if (value == null || value.isBlank()) {
return null;
}
return Color.decode(value);
}
}
Then, a Matcher
. I generalized.
import com.google.inject.TypeLiteral;
import com.google.inject.matcher.AbstractMatcher;
// Omitted: other imports
public class SubclassMatcher extends AbstractMatcher<TypeLiteral<?>> {
private final Class<?> type;
public SubclassMatcher(Class<?> type) {
this.type = type;
}
@Override
public boolean matches(TypeLiteral<?> toType) {
return toType.getRawType().isAssignableFrom(type);
}
}
Finally, add the following line to the Guice module.
import com.google.inject.AbstractModule;
// Omitted: other imports
public class ExampleModule extends AbstractModule {
@Override
protected void configure() {
binder().convertToTypes(new SubclassMatcher(Color.class), new ColorTypeConverter());
// Omitted: other configurations
}
}
Now, the following injection works.
public class Example {
@Inject
@Named("com.example.background-color")
private Color backgroundColor;
}
It could be prettier. There exists a com.google.inject.matcher.Matchers
API which I wasn't able use and could have solved my problem without constructing my personal SubclassMatcher
class. See, Matchers.subclassesOf(Class<?>)
. It's for sure my fault as I don't believe Google wouldn't think of this pretty common use-case. If you find a way to make it work, please leave a comment.