HOCON — конфигурируем гибко
Хранение параметров программ в текстовых конфигах — задача довольно частая и на первый взгляд тривиальная. Многие тут же хмыкнут:, а в чем проблема-то? Есть куча форматов (и библиотек для работы с ними): properties, XML, JSON, YAML. В чем хочешь — в том и храни. Делов-то.
Однако масштабы вынуждают посмотреть на это иначе. В частности, после многолетней разработки игровых серверов на Java я постепенно пришел к выводу, что управление конфигами не настолько уж банально. В этой статье речь пойдет о библиотеке HOCON — какие возможности она предоставляет и почему в последнем проекте мы стали пользоваться именно ей.
HOCON — это opensource-библиотека на Java, читающая конфиг-файлы из формата, основанного на JSON. По сравнению с JSON этот формат менее строгий и обладает дополнительными возможностями:
{
// Можно писать комментарии
"a" : 42 // Можно пропускать запятые в конце строки
b: hello // Можно пропускать кавычки
}
Однако основная ценность HOCON — это копирование значений переменных и даже целых JSON-объектов.
{
a: 42
b: ${a} // Присваиваем переменной b значение переменной a
c : {
m1 : 1
m2 : 2
}
d : ${c} { // Копируем в d значение из c
m3 : 3 // Добавляем в d переменную m3 = 3
}
}
Это, конечно, прикольно, скажет тут читатель, но зачем мне это надо? К чему городить весь этот огород вместо того, чтобы хранить свои конфиги в обычном JSON или XML? Чтобы ответить на этот резонный вопрос, поделюсь двумя примерами из нашей рабочей практики.
Пример 1. Из жизни админов
Мы разрабатываем игровые серверы. А игровые серверы — это целый зоопарк сервисов, которые в зависимости от требований могут работать на разном наборе железа и в различных раскладках. В качестве примера приведу схему раскладки сервисов по хостам с одного из моих прошлых проектов:
И у всех этих сервисов, разумеется, надо настраивать целую кучу параметров: тут и всякие сетевые адреса, и имена баз данных, пользователи, доступы и бог знает что еще. Параметров этих приблизительно 100500.
Пусть для примера у нас есть три сервиса s1, s2, s3, у которых надо настроить IP-адрес:
{
s1 :
{
ip: "192.168.10.1”
}
s2 :
{
ip: "192.168.10.1”
}
s3 :
{
ip: "192.168.10.1”
}
}
Очень часто эти сервисы запускаются на одном и том же хосте и имеют один и тот же IP-адрес. И мы не хотим при смене IP-адреса лазить по всему конфигу и везде их менять (помним о том, что в жизни их не три, а 100500). Что же делать? Если хранить конфиг в обычном JSON, то можно было бы завести общий параметр host_ip и написать примерно такой код:
ip = config.getValue("s1.ip”);
if ( ip == null ) {
ip = config.getValue("host_ip”);
}
Однако такое решение имеет существенные недостатки:
- Разработчик сервиса может его не предусмотреть для данного конкретного случая.
- Эта логика скрыта от администратора, который настраивает конфиги. Откуда ему знать, что если параметр s1.ip не указан, то он будет взят из параметра host_ip? Если же параметров много и они станут часто выделывать подобные фокусы, то с администратором может случиться сердечный приступ (и его тень будет по ночам являться разработчику).
На HOCON же решение полностью прозрачно:
{
host_ip: "192.168.10.1”
s1 :
{
ip: ${host_ip}
}
s2 :
{
ip: ${host_ip}
}
s3 :
{
ip: ${host_ip}
}
}
Пример 2. Из жизни разработчиков
При разработке задача развернуть новый сервер с нуля возникает не так уж и редко. Вывели новую ветку. Наняли нового программиста. Полетел старый жесткий диск, и надо все ставить заново. Поскольку локальный сервер — это тоже полноценный зоопарк со всеми сервисами (хоть и по одной штуке), то, конечно же, настраивать конфиг каждый раз с нуля совсем не хочется. Напрашивается вариант хранить общий конфиг в системе контроля версий вместе с кодом, а при развертке локального сервера менять в нем только IP хоста. Ну что ж, воспользуемся решением из прошлого примера:
{
host_ip: "192.168.10.1”
s1 :
{
ip: ${host_ip}
}
s2 :
{
ip: ${host_ip}
}
s3 :
{
ip: ${host_ip}
}
}
Но тут возникает загвоздка: общий конфиг-то лежит под системой контроля версий! Если я поменяю в нем host_ip, то возникнет куча неудобств: постоянно висит дифф, надо мержить изменения, внесенные другими. Еще не дай бог случайно закоммитишь свой host_ip в общую версию. Можно также хранить общий конфиг где-то в другом месте и подкладывать в нужное. Но как тогда туда будут попадать изменения, сделанные в версии другими разработчиками?
И тут нам на помощь приходит директива include:
{
host_ip: "127.0.0.1” // Пусть по умолчанию открываются локально
s1 :
{
ip: ${host_ip}
}
s2 :
{
ip: ${host_ip}
}
s3 :
{
ip: ${host_ip}
}
include "local_config.conf” // Подключаем параметры из файла local_config.conf
}
А дальше рядом с основным конфиг-файлом мы подкладываем файл local_config.conf с таким содержимым:
{
host_ip: "192.168.10.1” // Переопределяем IP на наше значение
}
Файл local_config.conf игнорируется системой контроля версий, никаких конфликтов не происходит.
Заключение
Рассмотренные примеры использования взяты из реальной разработки. И конечно же, возможности HOCON ими не ограничиваются. Фактически HOCON — это не просто текстовый формат, а, скорее, узкоспециализированный скрипт для конфигов, который может существенно облегчить жизнь администратору и разработчикам.
Приводить более подробное описание я тут специально не стал: все прекрасно изложено в официальном руководстве. Если у вас есть свои достойные внимания случаи использования этой библиотеки — делитесь ими в комментариях!