⬆️ ⬇️

Flexible configuration with Guice

There are many different configuration libraries available in Java, for example, one from Apache Commons , but they usually follow a very simple pattern: parsing a number of configuration files and building on the basis of this data Property or Map, which later values ​​are requested:

Double double = config.getDouble("number"); Integer integer = config.getInteger("number"); 


But this approach does not suit me for several reasons:



Some time ago I was reading the Guice documentation and came across a paragraph that suggested that it could be done better. Here is the relevant passage:

Guice supports binding annotations with attributes.

In those rare cases where you need them:

1) Create an @interface annotation.

2) Create a class that implements the annotation interface. Follow the guidelines for equals () and hashCode () defined in Annotation Javadoc. Pass the class instance to annotatedWith ().



And then there was the idea that using this technique, you can get exactly what I need - to create a more "smart" configuration framework, although I had plans not related to the annotatedWith stunt. The relevance of this method will become clear a little later, but for now we outline the main objectives.



Goals


I would like to implement:



It does not matter to me what external interface will be used: how the settings are received is not relevant to the framework, they can be in the form of XML, JSON, come over the network or from the database. At the entrance of the Map framework from the settings and I get them from there.



By the time we finish, we can do something like:

 # - properties- host=foo.com port=1234 


We use these values ​​in the code:

 public class A { @Inject @Prop(Property.HOST) private String host; @Inject @Prop(Property.PORT) private Integer port; // ... } 


Implementation



The definition of the Prop annotation is trivial:

 @Retention(RUNTIME) @Target({ ElementType.FIELD, ElementType.PARAMETER }) @BindingAnnotation public @interface Prop { Property value(); } 


Property is an enum containing all the information necessary for your settings. For the example above:

 public enum Property { HOST("host", "The host name", new TypeLiteral<String>() {}, "foo.com"), PORT("port", "The port", new TypeLiteral<Integer>() {}, 1234); } 


The enumeration contains a string field "property name", a description, a default value and its type. Note that this type is TypeLiteral, so we can describe properties that even have generic types that would otherwise be erased, a trick that allows the introduction of caches and other generic collections. Obviously, you can add additional parameters at your discretion (for example, “deprecated”).

')

The next step is to bind all the properties that we parse at the entrance — let's call the Map “allProps” —in our module so that Guice understands how to inject them.

In order to do this, we will sort out all these properties and tie them to our provider. Since we use typed fields, note the use of Key.get () from the Guice API, which allows you to map a property to the corresponding annotation:

  for (Property prop : Property.values()) { Object value = PropertyConverters.getValue(prop.getType(), prop, allProps.asMap()); binder.bind(Key.get(prop.getType(), new PropImpl(prop))) .toProvider(new PropertyProvider(prop, value)); } 


In the example there are three classes that I have not yet explained. The first, PropertyConverters, simply reads the property as a string and converts it to a Java type. The second is PropertyProvider, the simplest provider from Guice:

 public class PropertyProvider<T> implements Provider<T> { private final T value; private final Property property; public PropertyProvider(Property property, T value) { this.property = property; this.value = value; } @Override public T get() { return value; } } 


PropImpl is a bit more complicated and it stopped me all the time when I developed such a framework until I came across that tidbit in the Guice documentation. To understand the need for this class, you need to know how Key.get () works. Guice uses it to map types to unique keys that are used to inject the desired values. The important part here is that the method works not only with Class and TypeLiteral, but is also tied to the corresponding annotation. This annotation may be @Named , although I am not a big fan of it, because it works with strings, which means it is prone to typing errors, or its own annotation, this will suit us more. However, annotations in Java are a special thing; you cannot just get an instance of it.



This is where the trick described at the beginning of the article comes into play: Java, in fact, allows you to implement an annotation with a regular class. The implementation turned out to be quite simple, the difficulty was in realizing that this was all possible.



Now all the pieces are in place, it remains to analyze what magic is here:

  @Inject @Prop(Property.HOST) private String host; 


When Guice gets to this point of the injection, he discovers in his store a few bindings to strings, because they were tied to Key , which is essentially a pair (String, Prop) . In this case, it will look for a pair of String, Property.HOST and find there a provider that has been instantiated with a value from the property file, which it returns.



Generalize



I used to have this code compiled in one place, but after thinking about it, I decided to turn my miniframe library into a library so that others could use it. The only missing element was the ability to define more general Prop annotations. In the example above, this annotation contains a value of type Property, which is specific to my application:

 @Retention(RUNTIME) @Target({ ElementType.FIELD, ElementType.PARAMETER }) @BindingAnnotation public @interface Prop { Property value(); } 


In order to make it more universal, I had to return Enum:

 @Retention(RUNTIME) @Target({ ElementType.FIELD, ElementType.PARAMETER }) @BindingAnnotation public @interface Prop { Enum value(); } 


Unfortunately, this Java does not allow it according to JLS section 8.9, Enum and its generic version are not enumerated types, this is confirmed by J. Bloch.

Thus, it will not be possible to convert to the library, but if you are interested in using it in your project, copy the source code and modify it to suit your needs, starting with Prop#value according to your configuration.



A small proof of concept here , I hope you will find useful.

Source: https://habr.com/ru/post/187758/



All Articles