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

от автора

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

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

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


Статья и сама библиотека nProperty написана моим другом и товарищем по цеху Yorie для внутрикомандных повседневных нужд, и так как он, к сожалению, не имеет в данный момент инвайта на хабре, я взял на себя смелость, с его согласия, опубликовать сие творение для «хабровских» масс.

Содержание

  1. Просто о главном
  2. Чтение примитивных и стандартных типов
  3. Десериализация в массивы и коллекции
  4. Десериализация в пользовательские типы
  5. Модификаторы уровней доступа
  6. Инициализация всех членов класса
  7. Значения по умолчанию
  8. Переопределение имен
  9. Работа с не статичными полями классов
  10. Использование методов
  11. Обработка событий
  12. Использование потоков и дескрипторов файлов
  13. Недостатки
  14. Лицензия
  15. Ссылки

Просто о главном

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

SOME_INT_VALUE = 2 SOME_DOUBLE_VALUE = 1.2 SOME_STRING_VALUE = foo SOME_INT_ARRAY = 1;2;3 

Пример №1. Загрузка конфигурации с помощью стандартного класса Properties.

public class Example1 { 	private static int SOME_INT_VALUE = 1; 	private static String SOME_STRING_VALUE; 	private static int[] SOME_INT_ARRAY; 	private static double SOME_DOUBLE_VALUE;  	public Example1() throws IOException 	{ 		Properties props = new Properties(); 		props.load(new FileInputStream(new File("config/example.ini")));  		SOME_INT_VALUE = Integer.valueOf(props.getProperty("SOME_INT_VALUE", "1")); 		SOME_STRING_VALUE = props.getProperty("SOME_STRING_VALUE"); 		SOME_DOUBLE_VALUE = Double.valueOf(props.getProperty("SOME_DOUBLE_VALUE", "1.0"));  		// Предположим, что в настройках находится список целых через точку с запятой 		String[] parts = props.getProperty("SOME_INT_ARRAY").split(";"); 		SOME_INT_ARRAY = new int[parts.length]; 		for (int i = 0; i < parts.length; ++i) 		{ 			SOME_INT_ARRAY[i] = Integer.valueOf(parts[i]); 		} 	}  	public static void main(String[] args) throws IOException 	{ 		new Example1(); 	} } 

Пример №2. Загрузка конфигурации с помощью nProperty.

@Cfg public class Example2 { 	private static int SOME_INT_VALUE = 1; 	private static String SOME_STRING_VALUE; 	private static int[] SOME_INT_ARRAY; 	private static double SOME_DOUBLE_VALUE;  	public Example2() throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException 	{ 		ConfigParser.parse(Example2.class, "config/example.ini"); 	}  	public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IOException, IllegalAccessException 	{ 		new Example2(); 	} } 

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

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

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

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

public class Example3 {     @Cfg     private static int SOME_INT_VALUE;      @Cfg     private static short SOME_SHORT_VALUE;      @Cfg     private static long SOME_LONG_VALUE;      @Cfg     private static Double SOME_DOUBLE_VALUE;      /* ... */ } 

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

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

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

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

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

/*     В файле конфигурации находится следующее:         SOME_INT_ARRAY = 1--2--3         SOME_SHORT_ARRAY = 3>2<1         SOME_BIGINTEGER_ARRAY = 1;2;3  */ public class Example5 {     @Cfg(splitter = "--")     private static int[] SOME_INT_ARRAY;     @Cfg(splitter = "[><]")     private static short[] SOME_SHORT_ARRAY;     @Cfg     private static BigInteger[] SOME_BIGINTEGER_ARRAY; } 

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

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

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

public class Example6 {     @Cfg     private static List<Integer> SOME_ARRAYLIST_COLLECTION = new ArrayList<>();      @Cfg     private static List<Integer> SOME_LINKEDLIST_COLLECTION = new LinkedList<>(); } 

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

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

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

public class Example8 {     private static class T     {         private final String value;          private T(String value)         {             this.value = value;         }          public String getValue() { return value; }     }      @Cfg     private static T CUSTOM_CLASS_VALUE; } 

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

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

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

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

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

@Cfg public class Example7 {     /* Все поля класса будут использованы как поля для чтения настроек */     private static int SOME_INT_VALUE = 1;     private static String SOME_STRING_VALUE;     private static int[] SOME_INT_ARRAY;     private static double SOME_DOUBLE_VALUE;     private static List<Integer> SOME_ARRAYLIST_COLLECTION = new ArrayList<>();     private static List<Integer> SOME_LINKEDLIST_COLLECTION = new LinkedList<>();      @Cfg(ignore = true)     private final static Logger log = Logger.getAnonymousLogger(); } 

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

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

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

/* Файл конфигурации не содержит свойства WRONG_PROPERTY */ @Cfg public class Example9 {     private int WRONG_PROPERTY = 9000;      private int SOME_INT_VALUE; } 

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

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

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

public class Example10 {     @Cfg("SOME_INT_VALUE")     private int myIntValue; } 

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

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

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

@Cfg public class Example11 {     private static int SOME_SHORT_VALUE;      private int SOME_INT_VALUE;      public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException     {         ConfigParser.parse(Example11.class, "config/example.ini"); // В данном вызове не будет использоваться переменная SOME_INT_VALUE          ConfigParser.parse(new Example11(), "config/example.ini");     } } 

Как видно, в примере использованы два разных вызова одного и того же метода. После отработки метода 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 позволяет работать не только с полями, но и с методами:

public class Example12 {     private static List<Integer> VALUE_CHECK = new ArrayList<>();      @Cfg("SOME_INT_ARRAY")     private void checkIntArray(String value)     {         String[] values = value.split("--");          for (String val : values)         {             try             {                 /* ограничим значение промежутком [0,100] */                 VALUE_CHECK.add(Math.max(0, Math.min(100, Integer.parseInt(val))));             }             catch (Exception ignored) {}         }     } } 

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

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

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

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

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

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

Поддерживаемые события:

  • onStart(String path) — отправляется перед началом загрузки файла конфигурации
  • onPropertyMiss(String name) — вызывается в случае, если некоторая именованная конфигурация не была найдена в файле настроек, но была обозначена в классе аннотацией @Сfg
  • onDone(String path) — вызывается при завершении загрузки файла конфигурации
  • onInvalidPropertyCast(String name, String value) — вызывается в случае, когда удалось прочитать значение настройки из файла конфигурации, но не удалось привести это значение к типу соответствующего поля класса
@Cfg public class Example13 implements IPropertyListener {     public int SOME_INT_VALUE;     public int SOME_MISSED_VALUE;     public int SOME_INT_ARRAY;      @Override     public void onStart(String path)     {      }      @Override     public void onPropertyMiss(String name)     {      }      @Override     public void onDone(String path)     {      }      @Override     public void onInvalidPropertyCast(String name, String value)     {      }      public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException     {         ConfigParser.parse(new Example13(), "config/example.ini");     } } 

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

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

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

@Cfg public class Example14 {     public int SOME_INT_VALUE;     public int SOME_MISSED_VALUE;     public int SOME_INT_ARRAY;      public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException     {         ConfigParser.parse(new Example14(), "config/example.ini");         ConfigParser.parse(new Example14(), new File("config", "example.ini"));         ConfigParser.parse(new Example14(), new FileInputStream("config/example.ini"), "config/example.ini");     } } 

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

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

Недостатки

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

Лицензия

Разработка библиотеки не преследовала коммерческих целей и преследовать не будет.

Ссылки

Будем использовать? 🙂

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

Проголосовал 1 человек. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/194658/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *