...

четверг, 25 июля 2013 г.

[Перевод] Гибкая конфигурация с Guice

Существует множество различных конфигурационных библиотек, доступных в 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 здесь, надеюсь найдете полезным.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий