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 здесь, надеюсь найдете полезным.
ссылка на оригинал статьи http://habrahabr.ru/post/187758/
Добавить комментарий