Существует множество различных конфигурационных библиотек, доступных в Java, например, одна от Apache Commons, но они как правило, следуют очень простому шаблому: парсинг ряда конфигурационных файлов и построение на основе этих данных Property или Map, у которого в дальнейшем и запрашиваются значения:
Double double = config.getDouble("number");
Integer integer = config.getInteger("number");
Но этот подход меня не устраивает по нескольким причинам:
- Во-первых, получается достаточно многословно;
- Приходится передавать конфигурационный объект целиком, даже если от него требуется всего один параметр;
- Очень просто допустить ошибку в ключе и прочитать некорректные данные.
Некоторое время назад я читал документацию Guice и наткнулся на параграф, натолкнувший на мысль, что это можно делать лучше. Вот релевантный отрывок:
Guice поддерживает связывающие аннотации, имеющие аттрибуты.
В тех редких случаях, когда вам они требуются:
1) Создайте аннотацию@interface
.
2) Создайте класс, реализующий интерфейс аннотации. Следуйте гайдлайнам в отношении equals() и hashCode(), определенным в Annotation Javadoc. Передайте экземпляр класса в annotatedWith().
И тут появилась мысль, что используя эту технику, можно получить в точности то, что мне требуется — создать более «умный» конфигурационный фреймворк, хотя у меня были планы не связанные с трюком annotatedWith. Актуальность этого метода станет ясна чуть позже, а пока наметим основные цели.
Цели
Хотелось бы реализовать:
- Возможность инжекта индивидуальных конфигурационных значений в мою кодовую базу, по возможности типобезопасно. Никаких
@Named
и прочих строковых идентификаторов; - Список всех свойств, доступных приложению, вместе с типами, значениями по-умолчанию, документацией и возможностью дальнейших улучшений (например, настройка обязательна/опциональна, автоматическое определение неиспользуемых настроек, пометка настроек нерекомендуемыми и т.д.).
Мне не важно какой будет использоваться внешний интерфейс: как настройки получены — не релевантно фреймворку, они могут быть в виде XML, JSON, приходить по сети или из базы данных. На входе фреймворка Map из настроек и я получаю их оттуда.
К тому времени, как мы закончим, сможем сделать что-то вроде:
# Какой-то properties-файл
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();
}
Property — это enum, содержащий всю информацию, необходимую для ваших настроек. Для примера выше:
public enum Property {
HOST("host",
"The host name",
new TypeLiteral<String>() {},
"foo.com"),
PORT("port",
"The port",
new TypeLiteral<Integer>() {},
1234);
}
Перечисление содержит строковое поле «имя свойства», описание, значение по-умолчанию и его тип. Обратите внимание, что этот тип — TypeLiteral, так мы сможем описать свойства, имеющие даже generic-типы, которые в противном случае были бы стерты, трюк позволяет введение кэшей и других generic-коллекций. Очевидно, что вы сможете добавить дополнительные параметры по своему усмотрению (например, «deprecated»).
Следующим шагом будет привязка всех свойств, которые мы распарсили на входе — назовем Map «allProps» — в наш модуль так, чтобы Guice понял, как инжектить их.
Для того чтобы сделать это, мы переберем все эти свойства и привяжем их к своему провайдеру. Поскольку мы используем типизированные поля, то обратите внимание на использование Key.get() из Guice API, который позволяет отобразить свойство на соответствующую аннотацию:
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. Чтобы понять необходимость в этом классе, следует узнать как работает Key.get(). Guice его использует для отображения типов на уникальные ключи, которые используются для инжекта нужных значений. Важная часть здесь в том, что метод работает не только с Class и TypeLiteral, но и привязан к соответствующей аннотации. Эта аннотация может быть @Named
, хотя я не большой ее фанат, потому что работает со строками, значит подвержена опечаткам, или собственная аннотация, это больше нам подойдет. Тем не менее, аннотации в Java — особая вещь, нельзя получить ее экземпляр просто так.
Это то место, где вступает в игру трюк, описанный в начале статьи: Java, на самом деле, позволяет реализовать аннотацию с обычным классом. Реализация оказалась достаточно простой, трудность заключалась в осознании, что это вообще возможно.
Теперь все части на местах, осталось проанализировать, что здесь за магия:
@Inject
@Prop(Property.HOST)
private String host;
Когда Guice попадает в эту точку инжекта, он обнаруживает в своем загашнике несолько биндингов к строкам, потому что они были привязаны к Key
, который по сути пара (String, Prop)
. В данному случае он будет искать пару String, Property.HOST
и найдет там провайдера, который был инстанцирован со значением из property-файла, что он и возвращает.
Обобщаем
Раньше этот код у меня был собран в одном месте, но подумав, решил превратить свой минифреймворк в библиотеку, чтобы и другие могли пользоваться. Единственным недостающим элементом была возможность определять более общие Prop аннотации. В примере выше эта аннотация содержит значение типа Property, которое специфично для моего приложения:
@Retention(RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@BindingAnnotation
public @interface Prop {
Property value();
}
Для того чтобы сделать его более универсальным, мне пришлось возвращать Enum:
@Retention(RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@BindingAnnotation
public @interface Prop {
Enum value();
}
К сожалению, это Java это не позволяет согласно JLS секции 8.9, Enum и его generic-вариант это не перечислимые типы, это подтверждает и J. Bloch.
Таким образом, в библиотеку преобразовать не получится, но если вы заинтересованы в использовании его в своем проекте, скопируйте исходный код и модифицируйте под свои нужды, начиная с Prop#value
в соответствии с вашей конфигурацией.
Небольшой Proof of Concept здесь, надеюсь найдете полезным.
Автор: Lucyfer