Search code examples
javaguice

Guice, Type Conversion When Getting Values from Properties


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?


Solution

  • 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.