Статьи

Гибкая настройка с Guice

В Java имеется довольно много библиотек конфигурации, таких как  эта, доступная от Apache Commons , и они, как правило, следуют очень похожему шаблону: они анализируют различные файлы конфигурации и, в конце концов, выдают структуру, подобную Property или Map, где Вы можете запросить свои значения:

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

Я всегда был недоволен этим подходом по нескольким причинам:

  • Много котельной плиты для извлечения этих параметров.
  • Наличие общего объекта конфигурации, даже если мне нужен только один параметр из него.
  • Очень легко ошибиться в наборе свойства и получить неправильные значения.

Некоторое время назад я читал  документацию Guice  и наткнулся на параграф, который заставил меня понять, что, возможно, мы могли бы добиться большего. Вот соответствующая выдержка:

Guice поддерживает привязки аннотаций, которые имеют значения атрибутов. В том редком случае, когда вам нужна такая аннотация:

Создайте аннотацию @interface.
Создайте класс, который реализует интерфейс аннотации. Следуйте инструкциям для equals () и hashCode (), указанным в аннотации Javadoc. Передайте экземпляр этого в условие привязки annotatedWith ().

Я подумал, что использование этой техники может быть именно тем, что мне нужно для создания более разумной структуры конфигурации, хотя у меня были другие планы, чем использование этого трюка с методом annotatedWith, как предлагается в этом параграфе. Актуальность этого фрагмента станет ясна позже, поэтому давайте начнем с целей.

Цели

Я бы хотел:

  • Иметь возможность вводить отдельные значения конфигурации где угодно в моей базе кода, и я хочу, чтобы это было безопасно для типов. Нет @Named или другого поиска на основе строк.
  • Имейте канонический список всех свойств, доступных для приложения, с их полным типом, значением по умолчанию, документацией и оставляя дверь открытой для улучшений (например, является ли эта опция обязательной или необязательной, обнаруживая, когда некоторые свойства нигде не используются, устаревание, создание псевдонимов) , и т.д…).

Меня не волнует внешний вид: то, как эти свойства собираются, не относится к этой среде, они могут быть получены из XML, JSON, сети, базы данных, и они могут иметь произвольно сложные правила разрешения и переопределения, давайте сохраним это для будущего поста. Входные данные этой структуры — Карта свойств, и я беру это оттуда.

К тому времени, когда мы закончим, мы сможем сделать что-то вроде этого:

# Some property file
host=foo.com
port=1234

Используя эти значения конфигурации в вашем коде:

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

Реализация

Определение аннотации Prop тривиально:

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

Свойство — это перечисление, которое собирает всю информацию, необходимую для всех ваших свойств. В нашем случае:

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

Это перечисление содержит строковое имя свойства, описание, его значение по умолчанию и его тип. Обратите внимание, что этот тип является TypeLiteral, поэтому мы можем даже предложить свойства, которые имеют универсальные типы, которые в противном случае были бы удалены, — хитрость, которая пригодится для вставки кэшей или других универсальных коллекций. Очевидно, что вы можете иметь дополнительные параметры по своему усмотрению (например, «логическое устаревание»).

Следующим шагом является связывание всех свойств, которые мы проанализировали в качестве входных данных — мы будем использовать карту с именем allProps — в нашем модуле, чтобы Guice знал, как их внедрить.

Для этого мы повторяем все эти свойства и привязываем их к своему поставщику. Поскольку мы используем типизированные имена, обратите внимание на использование Key.get из API Guice, который позволяет нам конкретно указывать каждое свойство с определенной аннотацией:

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));
}

В этом фрагменте кода есть три класса, которые я еще не объяснил. Первый — это PropertyConverters, который просто читает строковую версию свойства и преобразует ее в тип Java. Второй — PropertyProvider, тривиальный поставщик 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 более хитрый, а также единственное, что всегда мешало мне реализовать такую ​​платформу, пока я не наткнулся на этот неясный кусочек документации Guice, приведенной выше. Чтобы понять необходимость его существования, нам нужно понять, как работает Guice’s Key.get (). Guice использует этот класс для преобразования типа в уникальный ключ, который он может использовать для ввода правильного значения. Здесь важно отметить, что этот метод не только работает как с Class, так и с TypeLiteral (который мы используем), но ему также может быть дана конкретная аннотация. Этой аннотацией может быть @Named, которой я не большой поклонник, потому что она является строкой, поэтому подвержена опечаткам или реальной аннотации, чего мы и хотим. Тем не менее, аннотации — это особые звери в Java, и вы не можете получить их экземпляр именно так.

This is where the trick mentioned at the top of this article comes into play: Java actually allows you to implement an annotation with a regular class. The implementation turns out to be fairly trivial, the difficulty was realizing that this was possible at all.

Now that we have all this in place, let’s back track and dissect how the magic happens:

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

When Guice encounters this injection point, it looks into its binders and it finds multiple bindings forStrings. However, because they have all been bound with a Key, the key is actually a pair: (String, a Prop). In this case, it will look up the pair String, Property.HOST and it will find a provider there. This provider was instantiated with the value found in the property file, so it knows what value to return.

Generalizing

Once I had the basic logic in place, I wondered if I could turn this mini framework into a library so that others could use it. The only missing piece would be to allow the specification of a more general Propannotation. In the example above, this annotation has 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 this more general, I need to make this attribute return an enum instead of my own:

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

Unfortunately, this is not legal Java, because according to the JLS section 8.9, Enum and its generic variants are not enum types, something that Josh Bloch confirmed, to my consternation.

Therefore, this cannot be turned into a library, so if you are interested in using it in your project, you will have to copy the source and make a few modifications to adjust it to your needs, starting by havingProp#value have the type of the enum that captures your configuration.

You can find a small proof of concept here, which I hope you’ll find useful.

Note: this is a copy of the article I posted on our work blog.