Integration was done in three iterations. Each iteration has it's pros and cons, so I'll describe each one.
Iteration 1. Injecting Configuration.
First of all I have implemented guice provider which initialized configuration object. Provider was mapped into one of modules in singleton scope. Each class relying on configuration was begging for injection of configuration object. Then it was the object's work to configure itself properly.
I didn't like this approach right from the beginning. Let's go through pros and cons briefly:
Pros:
- Solution is simple and is very quick to implement.
- Each class relying on configuration became dependent on Configuration interface.
- Each class contains code that maps fields to configuration properties. Code is repeated from class to class.
- Testability is affected. All classes that need configuration must be provided with either special test configuration instance or appropriate mock.
Iteration 2. Named constant bindings.
That's where named annotations steps in together with constant bindings. We need few steps to perform to get things working.
- Initialize Configuration.
- Use bindConstant together with Names.named bindings to bind configuration properties to some names.
- Use @Inject annotation together with @Named annotations to annotate fields (or setters, that is event better) that should be injected with configuration values.
There are some methods to address this problem, and one of them is custom injection annotation.
Iteration 3. Annotation injection.
The main idea behind annotation injection is to create special annotation that will inject configuration properties to fields. As opposite to previous iterations it's not out-of-the-box solution. But it still worth trying.
OK, what we going to do is to create @InjectConfig annotation which is provided with name parameter. Name reflects property name in configuration. Also some kind of defaultValue field should be provided to handle injection of missing properties.
Custom annotation approach is described in integrating log4j example. We need to implement three components: Annotation itself, TypeListener object that will detect places where we should inject configuration properties and MembersInjector that will do injecting work for us. Let us start with annotation:
Name property should be filled with configuration property name. Rest of fields are used to provide default value that will be injected in case of missing configuration property. All default... are provided with default values so it is optional to specify. Converter attribute is used to provide converter from string to type expected by field. By default dummy implementation of String-to-String can be used. Iterface of converter is like that:
Next thing to create is TypeListener:
TypeListener should be populated with initialized Configuration instance in order to provide member injectors capable of injecting configuration properties. TypeListener's hear method is invoked on each class initialized by Guice injector. It iterates through all declared fields and provides injector for each field annotated by @InjectConfig. Guice injector will use provided member injector instance to inject field value. Member injector is the place where injection happens. No magic there, let's look into:
Well, this one is bigger than previous. Don't be afraid, I'll explain everything. When time comes to do field injection Guice calls injectMembers method providing it with instance under injection. InjectMembers retrieves @InjectConfig annotation from processed field. Now we can retrieve configuration property name and default values from annotation. Second step to do is to detect type of injected field. We can do it by using getType() method of field. Retrieving type is followed by many if-elses that are used to inject primitive types. If the field type isn't primitive than converters come into playground. For non-primitive field getProperty method is called. It retrieves specified configuration property (or default value on missing property) as string. After that converter instance is created. Convert method invoked on converter resulting in object capable for field injection. Then execution is passed back to injectMembers method and object become injected.
All necessary objects are ready. The only thing left is to tell Guice to use them. We should do this in Guice configuration module. It can look like this:
There are only two things we should do (or even one, because binding Configuration is fully optional). Look to line which starts with bindListener. It installs new type listener. Listener's hear() method is invoked on each detection of class that must be initialized with Guice.
What we get now is the configuration injection which is as simple as annotating field with @InjectConfig annotation:
Happy coding, folks!