Гибкая конфигурация с Guice

в 9:52, , рубрики: configuration, java, juice, properties, Программирование

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js