Developer Tales or everything about everything

21Сен/1315

Чтение конфигурационных файлов в Java. nProperty.

nproperty-logo

Многие разработчики сталкиваются с необходимостью чтения конфигурационных (*.ini, *.prop, *.conf, etc.) файлов в разрабатываемых приложениях. В Java есть стандартный класс Properties, с помощью которого можно очень легко загрузить ini-файл и прочитать его свойства. При большом объеме конфигурационных файлов чтение и запись настроек в объекты превращается в очень нудную и рутинную работу: создать объект Properties, конвертировать каждую настройку в нужный формат и записать его в поле.

Библиотека nProperty (Annotated Property) призавана упростить этот процесс, сократив примерно в два раза требуемый код для написания загрузчиков настроек.

Чтобы показать, каким образом возможно обещанное сокращение кода в два раза, ниже приведены два примера: в первом примере используется стандартный класс Properties, во-втором - nProperty.

В обоих примерах будет использован один и тот же файл конфигурации:

Пример №1:

Пример №2:

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

Чтение примитивных и стандартных типов

Во втором вышеприведенном примере стоит обратить внимание на аннотацию @Cfg. Она и является причиной сократившегося кода. Библиотека nProperty основана на аннотациях, которые могут быть применены к классам, полям и методам классов.

Чтобы прочитать из конфигурационного файла настройки, тип которых относится к примитивным, достаточно каждое поле класса обозначить аннотацией @Cfg:

nProperty поддерживает достаточно богатый набор стандартных типов:

  • Integer/int;
  • Short/short;
  • Double/double;
  • Long/long;
  • Boolean/boolean;
  • String;
  • Character/char;
  • Byte/byte;
  • AtomicInteger, AtomicLong, AtomicBoolean;
  • BigInteger, BigDecimal.

Все эти перечисленные типы могут быть использованы в примере выше.

Десериализация в массивы и коллекции

Помимо стандартных типов также возможна десериализация в массивы с одним условием - тип массива должен принадлежать множеству стандартных типов:

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

Обратите внимание на аннотации у SOME_INT_ARRAY и SOME_SHORT_ARRAY. По умолчанию nProperty использует в качестве разделителя символ ";". Его можно легко переопределить, указав в аннотации к полю свойство splitter. И, как можно заметить, разделителем может выступать полноценное регулярное выражение.

Помимо массивов возможно использование коллекций, а именно - списков. Здесь необходмым является одно условие - коллекция должна быть обязательно проинициализирована до запуска чтения конфигурации. Это связано с тем, что экземпляры объектов коллекций могут быть разными (ArrayList, LinkedList и т.д.):

В остальном для коллекций сохраняются все свойства десериализации массивов.

Десериализация в пользовательские типы

В качестве дополнительной функции библиотека может работать с пользовательскими классами. Пользовательский тип обязательно должен иметь конструктор: MyClass(String), в противном случае будет вызвано исключение. Уровень видимости конструктора не имеет значения, он может быть как public, так и private:

Как видите, библиотеке все равно, что нужный конструктор обозначен модификатором private. В результате в поле value класса T будет записано значение из файла конфигурации.

Очевидно, что в качестве пользовательского типа можно использовать и стандартные классы Java. Например, можно инициализировать объекты java.io.File, загружая пути из конфигурационных файлов:

Таким образом, в данном примере переменной MY_FILE_PATH будет соответствовать объект, инициализированный с помощью стандартного конструктора File(String).

Модификаторы уровней доступа

Стоит отметить, что библиотеке nProperty абсолютно все равно, какие модификаторы доступа имеет поле, метод или конструктор - библиотека работает через механизм Reflections и управляет этими модификаторами самостоятельно. Конечно же, вмешательство в модификаторы никак не коснется других частей приложения, к которым библиотека отношения не имеет.

Инициализация всех членов класса

В предыдущих примерах видно, что при большом количестве полей в конфигурации придется написать большое кол-во аннотаций @Cfg. Чтобы избежать этой рутинной работы nProperty позволяет добавить аннотацию к самому классу, тем самым обозначив все поля класса как потенциальные поля для записи в них настроек из файла конфигурации:

Здесь стоит обратить внимание на член класса log. Ему назначена аннотация @Cfg с включенным свойством ignore. Это свойство означает, что данное поле не будет использоваться библиотекой при чтении конфигурации, а попросту будет пропущено. Данное свойство следует использовать только в случае, когда аннотация дейстует на весь класс, как показано в примере выше.

Значения по умолчанию

Одно из замечательных свойств библиотеки в том, что если свойство отсутствует в файле конфигурации, то поле класса никогда не будет изменено. Это позволяет легко высталять значения по умолчанию прямо в декларации поля класса:

В данном случае после парсинга конфигурации в поле WRONG_PROPERTY будет храниться все то же значение 9000.

Переопределение имен

В случаях, когда имя поля класса не совпадает с именем конфигурации в конфигурационном файле, его можно принудительно переопределить:

Естественно, если есть возможность сохранять равнозначность имен в коде и в файлах конфигурации, то лучше так и делать - это избавит от необходимости аннотировать каждое поле класса.

Работа с нестатичными полями классов

Библиотека способна работать как с классами, так и с их экземплярами. Это определяется путем различных вызовов метода ConfigParser.parse():

Как видно, в примере использованы два разных вызова одного и того же метода. После отработки метода ConfigParser.parse(Example11.class, "config/example.ini") в SOME_INT_VALUE будет нуль, причем это совершенно не зависит от файла конфигурации, потому что данное поле не является статичным и не может быть использовано без экземпляра объекта.

Сразу после второго вызова ConfigParser.parse(new Example11(), "config/example.ini") поле SOME_INT_VALUE для созданного объекта примет значение в соответствии с содержанием файла конфигурации.

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

Использование методов

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

  1. самостоятельно проверить или изменить значение после того, как библиотека проанализирует файл настроек и заполнит все поля класса;
  2. создать в качестве типа свой класс-обертку с конструктором (как было показано выше);
  3. исключить поле класса из списка свойств и назначить его методу.

Самый удобный и корректный способ - №3. Библиотека nProperty позволяет работать не только с полями, но и с методами:

Здесь в метод checkIntArray(String) в качестве первого параметра будет передано значение SOME_INT_ARRAY из файла конфигурации. Это очень удобный механизм для случаев, когда стандартные решения библиотеки не подходят. В методе-обработчике можно делать все, что угодно.

Однако, стоит отметить, что в случае работы с методами библиотека не использует механизм разделителей, то есть, на данный момент невозможно организовать автоматическое разбиение свойства в массив.

Как и прежде поддерживается преобразование типов, если тип первого параметра метода отличен от String.

Как и с полями класса, если имя метода эквивалентно имени настройки в файле конфигурации, то можно опустить задание имени в аннотации.

Обработка событий

Библиотека nProperty позволяет обрабатывать некоторые события во время чтения конфигурации. Для того, чтобы реализовать обработку событий, необходимо реализовать интерфейс IPropertyListener и все его методы. Вызов событий возможен только в случае работы с полноценными объектами, экземплярами класса, реализующего интерфейс IPropertyListener. Поддерживаемые события:

  • onStart(String path) - отправляется перед началом загрузки файла конфигурации;
  • onPropertyMiss(String name) - вызывается в случае, если некоторая именованная конфигурация не была найдена в файле настроек, но была обозначена в классе аннотацией @Cfg;
  • onDone(String path) - вызывается при завершении загрузки файла конфигурации;
  • onInvalidPropertyCast(String name, String value) - вызывается в случае, когда удалось прочитать значение настройки из файла конфигурации, но не удалось привести это значение к типу соответствующего поля класса.

В приведенном примере будут вызваны все 4 события. Событие onPropertyMiss будет вызвано из-за поля SOME_MISSED_VALUE, которое отсутствует в файле конфигурации. Событие onInvalidPropertyCast будет вызвано из-за неверного типа поля SOME_INT_ARRAY.

Использование потоков и дескрипторов файлов

Библиотека умеет принимать на вход не только имена файлов, также возможна передача объекта java.io.File, или потока данных, производного от абстрактного класса java.io.InputStream:

Как видно, в приведенном примере в случае работы с потоком, библиотека требует дополнительно указать название конфигурации, т.к. невозможно его получить из низкоуровнего объекта FileInputStream. Название не является важной частью и будет использовано библиотекой для отображения информации (в том числе, при работе с событиями).

Таким образом, данные могут быть получены не только из файловой системы, но и от любого источника данных, работающего по стандартам  Java. Умение работать с java.io.InputStream дает возможность библиотеке быть успешно примененной в операционных системах Android:

Дополнительные возможности

Использование префиксов

Иногда в конфигурационных файлах используются префиксы в качестве логических разделителей параметров. Например, вот так:

Для того, чтобы не указывать префиксы у каждого поля класса, предусмотрена возможность определить префикс для всех полей класса одновременно:

Параметр prefix, присвоенный аннотации класса, определяет префикс для всех полей: user, pswd, host, port. Чтобы отменить действие префикса, необходимо переопределить его, задав параметр prefix выбранному полю класса.

Параметризация

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

Параметром считается любая запись вида: ${some_name}, где some_name - имя поля из файла конфигурации, значение которого необходимо подставить.

Запись вида ${} не является параметром, однако, считается специальной последовательностью символов, которая в будет заменена на символ "$".

Рассмотрим пример задания SQL Link на основе имеющихся параметров:

Соответствующий файл загрузки:

Как видно из примера, для поля класса sqlink был включен параметр "parametrize". Здесь поле db.sqlink примет значение "mysql://user@127.0.0.1:90" и будет записано в поле класса sqlink.

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

приведет к тому, что итоговое значение поля prop3 будет равно "${prop1}bar". Это означает, что nProperty не обрабатывает циклические параметры. Чтобы получить желаемое значение "foobar" в данном примере, необходимо внести изменения следующим образом:

Начиная с версии 1.3.1 введена поддержка рекурсивной параметризации, и в предыдущем примере в свойстве prop3 будет содержаться значение "foobar".

Генерация файлов конфигурации

В библиотеку nProperty была добавлена возможность создавать файлы конфигурации на основе имеющихся Java-классов. Эта возможность может быть использована для автоматической генерации файлов настроек, для изменения настроек и сохранения изменений в файл. Особенностью является то, что nProperty не выполняет сортировку над полями классов, в отличие от java.util.Properties.

Для того, чтобы сохранить текущее состояние класса или экземпляра класса в файл, необходимо вызвать всего один метод: ConfigParser.store(...). Этот метод имеет несколько реализаций:

В файл будут записаны все поля класса, отмеченные аннотацией @Cfg. Значения свойств будут соответствовать текущим значениям полей в классе или объекте. Значения по умолчанию также учитываются.

Поддержка XML

Начиная с версии 1.4 в nProperty введена поддержка загрузки и сохранения в XML. По стандарту Java, XML-файлы должны соответствовать по структуре следующему DTD-описанию:

Такое же DTD-описание используется в классе java.util.Properties. Загрузка и сохранение свойств в XML ничем не отличается от загрузки и сохранения свойств в ini-формате. Для работы с XML добавлен абсолютно идентичный API:

Функции API XML имеют постфикс "...Xml(...)".

XML API nProperty, так же как и при работе с ini-форматом, сохраняет порядок ключей в файле.

Замечания

У библиотеки есть только один недостаток - всвязи с ограничениями, накладываемыми JVM и невозможностью технической реализации, библиотека не способна менять значения полей, имеющих модификатор final.

Лицензия

Библиотека выпускается под лицензией Apache License v2.0;

 

Ссылки

Просмотров: 8706
Комментарии (15) Пинги (0)
  1. Удобная и маленькая библиотека, большое спасибо!

    Один момент следует подправить:
    Для чтения используется метод Properties.load(InputStream), который не может в кодировки.
    InputStreamReader решает эту проблему. По хорошему можно кодировку как параметр конфигпарсеру передать, но себе я просто прибил гвоздями к utf-8

    • Спасибо и Вам за пост :)
      Да, Вы правы, кодировку я не учел - не доводилось использовать в проекте что-либо иное, помимо UTF-8. В будущей версии добавится возможность передать библиотеке объект-наследник java.io.Reader.
      Также, по запросу одного хабражителя будет добавлена возможность загрузки конфигурации с использованием префиксов.

  2. Так собственно, utf-8 и не читается (точнее, читается только латинница).
    Из документации:
    The input stream is in a simple line-oriented format as specified in load(Reader) and is assumed to use the ISO 8859-1 character encoding; that is each byte is one Latin1 character. Characters not in Latin1, and certain special characters, are represented in keys and elements using Unicode escapes

  3. После некоторого времени пользования библиотекой, понял что у нее есть один большой недостаток: она в одну сторону работает. А сохранять конфиги с java.util.Properties так же неудобно как и читать.

    Не могли бы Вы добавить в библиотеку возможность сохранять конфиг? По той же логике, как они и читаются - серилиазовать в классе/объекте то что подлежит конфигурированию.

    Я переделал метод parse0 под это, в логике ничего особенного - во всех нужных ветках вместо чтения из проперти в поля, написал запись из полей в проперти... вот только сам java.util.Properties для записи конфигов никуда не годится, т.к. это хешмап и порядок - космический.

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

      • Пример использования: даже если в софте нету сохранения конфигурации, такой функционал можно использовать для создания заготовки конфига (н-р если файл конфига не существует - создаем его с дефолтами из кода).

  4. Прошу прощения только начинаю изучать java. А как с помощью Вашей библиотеки группировать параметры? Например:
    [Groups1]
    param1
    param2

    [Groups2]
    param1
    param2

    • К сожалению, стандартный парсер java.util.Properties, на основе которого разработана библиотека, не поддерживает группировки. Поэтому библиотека также не поддерживает группы, иначе пришлось бы писать свой собственный парсер ini-файлов.
      Более того, если не ошибаюсь, группы - это расширение стандарта ini-файлов Microsoft, в изначальной спецификации группы не предусматривались, поэтому их нет и в Java.

  5. А как можно скачать эту удивительную библиотеку?

  6. Добрый день!
    Что-то с репозиторием: ошибка 404 ((
    Подскажите, пожалуйста, как вашу замечательную библиотеку в maven добавить?


Leave a comment


четыре × = 24

http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_bye.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_good.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_negative.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_scratch.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_wacko.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_yahoo.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_cool.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_heart.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_rose.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_smile.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_whistle3.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_yes.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_cry.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_mail.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_sad.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_unsure.gif 
http://microfork.com/wp-content/plugins/wp-monalisa/icons/wpml_wink.gif 
 

Trackbacks are disabled.