Денисов В.С.
Московский государственный университет им. М.В.Ломоносова, ф-т ВМК, науч. сотр.
ФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ К БИБЛИОТЕКЕ УПРАВЛЕНИЯ НАСТРОЙКАМИ JAVA-ПРИЛОЖЕНИЙ
КЛЮЧЕВЫЕ СЛОВА
Программирование, Java, библиотека, фреймворк, аннотации, настройки. АННОТАЦИЯ
В статье рассматриваются общие и специальные требования к современной библиотеке управления настройками Java-приложений. Приводится краткое обоснование каждого требования.
Поддержка тех или иных способов настройки времени выполнения является неотъемлемой частью функционала подавляющего большинства приложений. Сформировавшийся за многие годы базовый подход к получению настроек достаточно прост — настройки считываются из некоторого постоянного хранилища (например, из файла на файловой системе, из таблицы в РСУБД, системного хранилища конфигурационной информации и т. п.) при старте приложения (а также, возможно, и после его старта — например, при изменении содержимого файла) и сохраняются во внутренних структурах (объектах) приложения, откуда в дальнейшем используются прочими программными компонентами. Если это необходимо, в ходе выполнения настройки могут также сохраняться в постоянном хранилище.
Формирование типового подхода к решению задачи обычно влечёт за собой разработку фреймворков, реализующих данный шаблон для различных программных платформ. Платформа Java не является исключением - в [1] описаны некоторые популярные библиотеки для работы с настройками, а также проблемы, возникающие при их применении.
К сожалению, библиотеки первого поколения, созданные, в основном, в 2000-2005 гг, в последние годы практически прекратили своё развитие, серьёзно отстав на концептуальном и техническом уровне от новейших возможностей платформы Java, таких как внедрение зависимостей, объектно-реляционные отображения, лямбда-выражения, аннотации, поддержка платформ облачных вычислений и т. п. Возникла потребность в формировании требований, а после этого и в разработке нового поколения библиотек управления настройками времени выполнения.
Всю совокупность требований к библиотеке управления настройками можно разделить на два больших класса: общие требования — требования, не относящиеся к специфике предметной области — и специальные, эту специфику учитывающие.
К общим требованиям относятся:
• независимость от целевой платформы;
• использование юнит- и интеграционных тестов;
• поддержка внедрения зависимостей.
К специальным требованиям относятся:
• минимальное количество зависимостей;
• описание конфигурационной информации с помощью аннотаций;
• самодокументация объектов конфигурации;
• поддержка облачных сервисов;
• безопасность типов (type safety);
• поддержка различных источников информации о настройках/постоянных хранилищ;
• расширяемость;
• поддержка сложных структур данных;
• поддержка валидации и конверсии значений;
• поддержка извещений об изменении настроек;
• поддержка структурированной информации.
Независимость от целевой платформы. Виртуальная машина Java может быть запущена на самых разных программных и аппаратных платформах — с различной архитектурой, операционными системами, системами хранения и т. п. Библиотека управления настройками должна делать как можно меньше предположений о специфике целевой платформы — например, о наличии у неё поддержки файловой системы, доступа к сети и т. п.
Использование юнит- и интеграционных тестов. Библиотека управления настройками предоставляет приложению критически важные сервисы — без получения информации о конфигурации времени выполнения полноценная работа приложения, как правило, не является возможной. Кроме того, в силу специфики функционала библиотеки она неизбежно будет содержать множество различных, слабо пересекающихся потоков вычислений (рассчитанных на различные окружения, источники данных, форматы конфигурационной информации и иные возможности библиотеки), подавляющее большинство которых не будет задействовано в любом конкретном операционном окружении. Учитывая эти особенности, работа библиотеки должна быть верифицирована набором автоматически исполняемых тестов, обеспечивающих адекватное покрытие исходного кода библиотеки (например, в [2] предлагается уровень покрытия в 85% строк).
Поддержка внедрения зависимостей. Внедрение зависимостей может применяться по отношению к библиотеке управления настройками двояко: с одной стороны, библиотека может получать внешние зависимости (например, зависящие от среды выполнения) от фреймворка внедрения зависимостей (или, в более общем случае, от контейнера инверсии контроля — IoC-контейнера), а с другой — предоставлять свои сервисы через подобный фреймворк остальным компонентам приложения. Это особенно важно для организации юнит-тестирования, когда сервисы, использование которых при тестировании неэффективно или сопряжено с финансовыми затратами, заменяются своими фиктивными копиями, которые внедряются в компоненты библиотеки.
Минимальное количество зависимостей. Библиотека управления настройками должна обеспечить простоту использования в проектах любой сложности, в том числе и в насчитывающих всего несколько сот строк кода. "Большие" проекты, как правило, строятся вокруг специализированной системы управления зависимостями, такой как Maven или Gradle (см. в [3] описание нескольких распространённых систем управления зависимостями для платформы Java), однако для "малых" проектов, зачастую, применение таких систем оказывается неоправданно сложным. Соответственно, базовая функциональность библиотеки — в идеале — вообще не должна иметь внешних зависимостей, а функциональность, использование которой невозможно без привлечения зависимостей (например, SDK для облачных сервисов) должна быть вынесена в набор дополнительных необязательных модулей.
Описание конфигурационной информации с помощью аннотаций. Аннотации в Java позволяют разработчику связать дополнительную семантическую информацию с определёнными синтаксическими элементами кода, такими как классы, методы, переменные-члены класса и т. д. Аннотации располагаются непосредственно в исходных текстах приложения, а при компиляции могут становиться частью байт-кода, сохраняя доступность (через механизм отражения) во время выполнения приложения. Всё это делает аннотации идеальным способом передачи подобной информации как между компонентами приложения, так и между компонентами приложения и внешними зависимостями. Для управления настройками, аннотации позволяют передать информацию, специфическую для данной области (например, информацию о том, является ли определённое свойство доступным только для чтения или для чтения и записи, определить способ отражения свойства при сохранении в постоянном хранилище и т. п.) Кроме того, аннотации — за счёт встраивания непосредственно в исходный текст программы — помогают создавать самодокументированный код (см. ниже).
Самодокументация объектов конфигурации. Когда новый разработчик начинает знакомство с настройками, доступными в приложении, у него возникает достаточно типовой набор вопросов:
• какие настройки/свойства доступны для приложения?
• какие свойства доступны на чтение? на чтение и запись?
• какие свойства сохраняются в постоянном хранилище, а какие имеют смысл только во
время выполнения приложения?
• какие значения (по типу, по диапазону) может принимать свойство?
• в каком постоянном хранилище сохраняются настройки, и доступно ли это хранилище только на чтение или на чтение и запись?
Удобно, когда ответы на эти вопросы могут быть просто определены непосредственно в исходном тексте приложения — самодокументация ускоряет разработку и уменьшает количество ошибок и разночтений при использовании объектов настроек.
Поддержка облачных сервисов. Эффективное использование современных облачных сервисов (а облачные сервисы используются более 90% компаний, см. [4]) требует серьёзных изменений в архитектуре приложений, затрагивая, в том числе, и работу с настройками — от отсутствия локальной файловой системы до отсутствия концепции "вычислительного узла" в некоторых видах облачных вычислительных сервисов. Приложения, конфигурируемые "в облаке", часто должны поддерживать нестандартные источники информации — в том числе специализированные облачные постоянные хранилища.
Безопасность типов. При хранении настроек внутри приложения в виде строковых объектов (а это до сих пор самый распространённый подход, благодаря классу java.util.Properties) часто возникает вопрос: "Какому типу Java на самом деле соответствуют значения определённого свойства?" Целый это тип или тип с плавающей либо фиксированной запятой? Или, возможно, в качестве значения также может использоваться собственно строка символов? Или это некий сложный объект, сериализованный в строку? Существующие библиотеки, к сожалению, практически не содержат способов определить тип свойства в момент разработки приложения. Соответственно, современная библиотека должна содержать средства, гарантирующие безопасность типов, как при чтении значений свойств, так и при их модификации. Для этих целей естественным образом подходят интерфейсные классы — если для чтения и модификации значений свойств используются только методы этого класса, то безопасность типов гарантируется компилятором Java.
Поддержка различных источников информации о настройках/постоянных хранилищ. Современные Java-приложения работают в самых различных операционных окружениях, на классических серверах и рабочих станциях, в облачных и встраиваемых платформах, на мобильных устройствах. В зависимости от окружения, настройки могут быть получены из различных источников (список, конечно, далеко не исчерпывающий):
• традиционные файлы настроек различного формата;
• переменные окружения (виртуальной машины Java и ОС);
• реляционные и нереляционные СУБД;
• облачные системы хранения документов;
• веб-сервисы.
Кроме того, в некоторых сценариях данные из различных источников могут комбинироваться друг с другом. Так, в облачном окружении AWS EC2 может использоваться следующая цепочка получения настроек:
• значения по умолчанию считываются из ресурса, расположенного внутри classpath;
• общие для всех виртуальных машин настройки считываются из документа в хранилище S3;
• специфические для виртуальной машины настройки считываются с помощью HTTP-запроса из метаданных виртуальной машины.
Библиотека управления настройками должна поддерживать чтение (и запись, в том случае, когда это технически возможно) различных форматов конфигурационной информации из различных источников — с динамическим выбором источника или цепочки источников в зависимости от операционного окружения, прозрачно для приложения.
Расширяемость. Поскольку получение настроек приложения неразрывно связано с операционным окружением приложения, сделать библиотеку управления настройками полностью самодостаточной невозможно — всегда найдётся особенность окружения (или использующего библиотеку приложения), для которой существующей стандартной функциональности библиотеки окажется недостаточно. Можно выделить по крайней мере две основные точки расширения функционала: доступ к постоянному хранилищу и формат конфигурационной информации. Библиотека должна определять стандартные механизмы расширения, позволяющие разработчикам добавлять необходимый функционал без модификации исходного кода библиотеки.
Поддержка сложных структур данных. Большинство существующих библиотек управления настройками поддерживают в качестве значений свойств только очень простые типы данных: числа, строки и, возможно, массивы/списки строк и чисел. Однако при конфигурации сложных приложений возникает потребность в отображении настроек на более сложные структуры данных. Библиотека должна предоставлять возможность отображать настройки на наиболее часто используемые структуры данных (включая Java Beans и коллекции из Java Collections Framework), а также предоставить приложению возможность расширения функциональности для реализации поддержки произвольных, сколь угодно сложных, типов данных.
Поддержка валидации и конверсии значений. Рано или поздно, библиотека обязательно столкнётся с ошибкой в конфигурационных данных — это может быть как ошибка формата (например, неверно сформированный XML-документ), так и недопустимое значение одного из свойств. Реакция библиотеки на подобные ошибки должна быть чёткой и предсказуемой: библиотека должна, в зависимости от требований приложения, или быстро и однозначно сигнализировать о наличии ошибки (например, путём бросания проверяемого исключения), или заменить значения "пострадавших" свойств некоторыми разумными значениями по умолчанию. Кроме того, должна поддерживаться возможность расширения поддерживаемых форматов данных путём добавления конвертеров, сериализующих/десериализующих отдельные свойства или настройки целиком.
Поддержка извещений об изменении настроек. Настройки приложения обычно используются одновременно в нескольких его модулях. Изменение настроек (как внешнее — например, изменение файла на файловой системе — так и внутреннее изменение свойства одним из компонентов) может быть интересно зависящим от этих настроек компонентов. Библиотека должна поддерживать извещение об изменениях настроек — или в виде традиционного шаблона провайдер/слушатель, или более эффективные реализации на базе шины событий.
Поддержка структурированной информации. Для крупных приложений, число параметров конфигурации может быть достаточно велико — несколько сотен или даже тысяч настроек. Однако каждый индивидуальный модуль приложения, скорее всего, работает с небольшим количеством настроек. Разработчикам модуля было бы удобно, если бы библиотека управления настройками могла бы предоставить данному модулю только необходимые ему свойства, скрыв нерелевантные. Кроме того, для постоянных хранилищ, поддерживающих структурированные данные (а большинство распространённых видов хранилищ поддерживают такие данные), было бы удобно аналогично читать и сохранять настройки с полным или частичным сохранением структуры.
Более подробно требования к библиотеке управления настройками изложены в [5]. Библиотека с открытым исходным кодом options-util, разработанная на основании этих требований, описана в [6].
Литература
1. V. Denisov. Overview of Java application configuration frameworks. International Journal of Open Information Technologies ISSN: 2307-8162, 1(6), 2013. URL: http://injoit.org/index.php/j1/article/view/33
2. L. Koskela. Test Driven. Practical TDD and Acceptance TDD for Java Developers. Manning, Greenwich, CT, 2008
3. M. Rasmussen. (2013, Nov. 21). Java Build Tools: How Dependency Management Works with Maven, Gradle and Ant + Ivy. URL: http://zeroturnaround.com/rebellabs/java-build-tools-how-dependency-management-works-with-maven-gradle-and-ant-ivy/
4. RightScale (2015). Cloud Computing Trends: 2015 State of the Cloud Survey. URL: http://www.rightscale.com/blog/cloud-industry-insights/cloud-computing-trends-2015-state-cloud-survey
5. V. Denisov. Functional requirements for a modern application configuration framework. International Journal of Open Information Technologies ISSN: 2307-8162 3(10), 2015. URL: http://injoit.org/index.php/j1/article/view/236
6. V. Denisov. Annotations-driven configuration framework for Java applications. International Journal of Open Information Technologies ISSN: 2307-8162 3(10), 2015. URL: http://injoit.org/index.php/j1/article/view/237.