О чём вообще речь?
Всем привет! В данной статье речь пойдёт о настраиваемых параметрах конфигурации Spring приложений. Когда я только начал изучать Spring, естественно, одним из источников знаний были готовые примеры, проекты-образцы. И меня жутко бесило, что какие-то нужные для работы приложения значения появлялись «ниоткуда». К примеру, автор какого-нибудь туториала предлагал для проверки только что созданного учебного приложения зайти на localhost по порту 8088. Откуда берётся 8088? Почему не 8089? Оказалось, что для таких настраиваемых параметров есть специальные файлы. Итак:
Какие бывают настраиваемые параметры?
Настраиваемые параметры используются самим Spring-ом, различными библиотеками и, по желанию разработчика, могут быть добавлены свои собственные. Список всех параметров Spring-а можно посмотреть здесь.
Например, за то на каком порту будет крутиться встроенный http-сервер (если мы используем Spring Web) отвечает параметр server.port. В том самом туториале из вступления в соответствующем файле server.port был равен 8088. Выглядит это (в простейшем случае) так:
server.port=8088
Имя параметра может состоять (и, как правило, состоит) из нескольких частей. Например, все «спринговые» параметры начинаются со слова «spring». Кастомные (пользовательские) параметры, введённые разработчиком конечного приложения, могут начинаться, например со слова application или любого другого. В зависимости от используемого формата файла, части разделяются по-разному (см. следующий раздел). Простейший вариант, просто точками. Пример пользовательских параметров:
# Личное дело любимой собаки
application.dog.name=Полкан
application.dog.breed=овчарка
application.dog.color=коричневый
# Имя любимого кота
application.cat.name=Мурзик
Какие бывают источники настраиваемых параметров?
Настраиваемые параметры хранятся в файле, который может называться по разному:
application.<…> — базовый вариант по умолчанию
<any-name> — имя файла и путь к нему определяется аннотацией @PropertySource
bootstrap.<...> — начальные значения параметров при использовании spring-cloud
<application-name>.<...> — совпадает с именем приложения, доступно через config-сервер (актуально для Spring Cloud)
Каждый из этих вариантов может быть в формате key/value или в yml(yaml) формате. В случае с key/value файл должен иметь расширение properties и, как можно догадаться, каждая строчка должна выглядеть как «key=value». Строки-комментарии начинаются с символа «#». Самый распространённым вариант, когда файл параметров называется application.properties.
Пример содержимого в предыдущем разделе.
Yml(yaml)-формат представляет собой дерево, где каждая часть имени параметра является именем узла, а значение листом. Имя каждого узла пишется с новой строки и заканчивается двоеточием. Каждый дочерний узел располагается на два пробела правее родительского. Строки-комментарии также начинаются с символа «#». Выглядит это всё как-то так:
application:
# Личное дело любимой собаки
dog:
name: Полкан
breed: овчарка
color: коричневый
# Имя любимого кота
cat:
name: Мурзик
...
Будьте внимательны, количество пробелов в отступах для yaml-формата имеет значение!
Первый вариант application.property или application.yml «из коробки» работает только в Spring Boot. В классическом Spring аннотация @PropertySource обязательна. Она ставится на любой configuration-класс (класс, уже помеченный аннотацией @Configuration), в котором предполагается доступ к настраиваемым параметрам, и выглядит так:
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { … }
Также, Spring Boot позволяет иметь несколько вариантов настроек в разных файлах. В этом случае имя файла должно соответствовать следующему шаблону: application.<profile-name>.properties, ну или application.<profile-name>.yml. Актуальный вариант будет выбран в зависимости от активного spring-профиля. Что это такое, в этой статье не рассматривается, но можно почитать, например, здесь.
Где находятся эти источники настраиваемых параметров?
По умолчанию такие файлы должны лежать в classpath. Чаще всего в папке «src/main/resources». Если есть желание сложить настройки в файл с другим именем и/или по другому пути, необходимо перед классом, помеченным аннотацией @Configuration или перед тем классом, в котором планируется использовать настраиваемые параметры, указать уже знакомую аннотацию @PropertySource с путём и именем файла параметров, например так:
@PropertySource("file:/home/alex/tmp/temporary.properties")
Также в качестве источника файлов конфигурации при использовании SpringBoot может выступать config-server. Как приложение узнаёт, по какому url-у искать config-server? Сначала оно находит локальный config для загрузки базовых параметров, как раз таких, как путь к config-server-у. Этот файл должен лежать в classpath и называться application.properties или application.yml или application.yaml. В зависимости от версии Spring Сloud, имя может быть не application, а bootstrap. Допустимые расширения имени файла такие же.
В этом файле Spring ищет параметр с именем (в формате через точку) «spring.cloud.config.uri» или «spring.cloud.config.discovery.serviceId». Пример:
spring.cloud.config.uri=http://localhost:8888
или
spring.cloud.config.discovery.serviceId=config
Второй вариант, в случае использования Registry and Discovery Service (что это за зверь, можно почитать здесь).
Как использовать настраиваемые параметры (получать к ним доступ)?
Есть несколько способов. С помощью аннотации @Value, аннотации @ConfigurationProperties или же с помощью спрингового интерфейса Environment, вернее, методов классов, которые он расширяет. Разберём подробнее все варианты.
@Value
Этот способ проще всего. Достаточно указать эту аннотацию перед полем класса и при создании bean-а Spring проинициализирует это поле значением из config-файла. Пример:
@Value("${application.dog.name}")
private String dogName;
Если конфиг такой, как в нашем примере из начала статьи, то после создания bean-а для класса, у которого есть поле dogName, это поле будет равно «Полкан». У аннотации @Value есть ещё одна очень полезная возможность. Перед закрывающей фигурной скобкой через двоеточие после имени параметра можно задать его дефолтное значение. Т.е., если в конфиге указанное свойство не найдётся, то поле, помеченное такой аннотацией будет равно этому значению. Пример:
@Value("${application.dog.size:маленькая собачка}")
private String dogSize;
Так как в нашем конфиге параметра application.dog.size нет, поле dogSize будет проинициализировано по-умолчанию значением «маленькая собачка».
@ConfigurationProperties
Здесь для доступа к свойствам, прописанном в config-е надо создать специальный класс, помеченный данной аннотацией. Далее, поля этого класса будут соответствовать свойствам config-а. Причём соответствие будет устанавливаться автоматически, на основании имён. Причём имена должны быть похожи «приблизительно». Последняя возможность называется Relaxed binding.
Для того, чтобы было удобнее, в аннотации можно задать префикс, с которого будут начинаться свойства из конфига, привязанные к полям аннотированного класса. Немного запутанно? Давайте посмотрим на примере. Config возьмём знакомый, про Полкана и Мурзика. Вот как будут выглядеть соответствующие классы:
@Data // Это аннотация lombok, которая автоматически генерирует getter-ы и setter-ы
@Configuration // Аннотация Spring, благодаря которой автоматически будет создан been
@ConfigurationProperties(prefix = "application.dog")
public class DogConfig {
private String name;
private String breed;
private String color;
}
@Data // Это аннотация lombok, которая автоматически генерирует getter-ы и
//setter-ы
@Configuration // Аннотация Spring, благодаря которой автоматически
//будет создан been
@ConfigurationProperties(prefix = "application.cat")
public class CatConfig {
private String name;
}
И доступ к нашим настройкам из классов-потребителей будет выглядеть так:
@Component
public class ConfigConsumer {
@Autowired
private DogConfig dogConfig;
@Autowired
private CatConfig catConfig;
public void printConfiguration() {
System.out.println(dogConfig.getName());
System.out.println(dogConfig.getBreed());
System.out.println(dogConfig.getColor());
System.out.println(catConfig.getName());
}
}
Ну и main:
@SpringBootApplication
// @EnableConfigurationProperties(AppConfig.class) - требуется, если
// НЕ Spring Boot
public class SpringPropertiesApplication {
public static void main(String[] args) {
ApplicationContext context =
SpringApplication.run(SpringPropertiesApplication.class, args);
context.getBean(ConfigConsumer.class).printConfiguration();
System.exit(0);
}
}
Интерфейс Environment
Spring автоматически создаёт bean типа Environment. И если его заавтоварить, то через него можно получить доступ и к настраиваемым параметрам и к переменным окружения среды исполнения приложения. Пример:
@Component
public class ConfigConsumer {
@Autowired
private Environment env;
public void printConfiguration() {
System.out.println(env.getProperty("HOME"));
System.out.println(env.getProperty("application.dog.name"));
}
}
Выполнение приведённого ранее main-а выведет в моём случае:
/home/alex
Полкан
Какие есть нюансы?
Аннотация @Value ставится на поле класса, аннотация @ConfigurationProperties — на класс.
Использовать настраиваемые параметры можно не везде. Например, в конструкторе задействовать их напрямую не получится. Это связано с жизненным циклом spring-приложения. Дело в том, что для того, чтобы параметры были доступны, spring должен обработать наши конфиги. А он делает это непосредственно перед созданием бинов, вернее, перед помещением их в контекст, уже после вызова конструктора. Тем не менее, если класс, в конструкторе которого мы хотим задействовать настраиваемый параметр, сам является бином spring (помечен аннотацией @Component или аналогичной) и конструктор с параметром, то это ограничение можно обойти. Выглядеть это будет так:
@Data
@Component // Bean Spring
public class ParamInConstructor {
private String priority;
@Autowired
public ParamInConstructor(@Value("${priority:normal}") String priority) {
this.priority = priority;
}
}
Имеется нюанс с национальными алфавитами. Все значения настраиваемых параметров в config-файлах должны быть в кодировке ISO 8859-1, иначе мы рискуем получить кракозябры в работающем приложении. К счастью, в современных IDE присутствует функция автоматической перекодировки, которая называется native-to-ascii. Если её включить, то об этой проблеме можно не думать. В IntelyJ Idea версии 2020.3.4 автоперекодировка включается здесь:
При определении параметра в config-е можно присвоить ему значение переменной окружения. Допустим, мы хотим иметь возможность, задать имя нашей любимой собаки в переменной окружения «DOG». Но, если те, кто будет разворачивать наше приложение полностью нам доверяют и не собираются создавать никаких переменных окружения, то должно подставляться имя по-умолчанию. Делается это так:
application.dog.name=${DOG:Полкан}
Значение параметра может быть многострочным. Для этого в месте переноса нужно вставить «\n», что означает перенос строки:
application.cat.characteristics=Big \n Brown Soft Lazy
И наоборот, возможна ситуация, когда значение параметра представляет собой одну, но очень длинную строку. Которая не влезает в ширину экрана. Для удобства записать её можно в нескольких строках, но параметр будет содержать всё в одной. Для этого в месте перехода на новую строку надо поставить обратный слэш «\»:
application.cat.characteristics=Big \
Brown \
Soft \
Lazy
Параметр application.cat.characteristics в примере будет содержать одну строку, хотя записан в нескольких.
Что осталось?
Ну, наверное, осталось ещё много всего, но то, что хотелось бы упомянуть, но на чём останавливаться подробно не будем:
Meta-data. В случае с доступом к настраиваемым параметрам через @ConfigurationProperties есть возможность использовать meta-data. Это подробная информация о настраиваемых параметрах, помогающая среде разработки (IDE) выдавать контекстные подсказки, предлагать автодополнение, предупреждать о несоответствии типов и т.д.
SpEL evaluation. Эта фича, наоборот доступна при использовании аннотации @Value. SpEL – Spring Expression Language – язык выражений Spring. Очень полезная возможность, позволяющая, например раскладывать значение параметра по элементам списка (ArrayList):
arrayOfStrings=cat, dog, bear
@Value("#{'${arrayOfStrings}'.split(',')}")
private List<String> listOfStrings;
Заключение
Данная статья не претендует на всеобъемлющее руководство по использованию настраиваемых параметров Spring. Скорее она призвана помочь «находить концы» в чужом коде тем, кто по каким-то причинам пытается в нём разобраться.
Полезные ссылки:
Настраиваемые параметры Spring
Сравнение @ConfigurationProperties и @Value
Работа с параметрами через @ConfigurationProperties
Spring Cloud: Registry and Discovery Service
Комментарии (7)
Kudesnik99 Автор
09.06.2023 06:28Я бы ещё сказал в пользу фреймворков вот что. Естественно, для того, чтобы прочитать свойство из конфига, подключать тонны зависимостей - это перебор. Но если фреймворк облегчает тучу действительно сложных и/или рутинных задач, то для решения таких задач использовать его имеет смысл. А если уж подключили, то почему бы не воспользоваться более мелкими удобствами, идущими в комплекте?
Ну и опять же, не всегда разработчик свободен в выборе инструментария. Рискну предположить, что поддержка занимает гораздо больше человеко-часов чем разработка новых продуктов. А по факту Spring сейчас де-факто индустриальный стандарт в бэкенде. Так что может нравиться, может нет, но очень многим дело с ним иметь приходится. Ну и, соответственно, возникает желание понимать "откуда что берётся"...pin2t
09.06.2023 06:28-1действительно сложных и/или рутинных задач
Какие такие там сложные задачи? Задачи-то на самом деле простые, только они невероятно сложно сделаны в spring-е, вот spring и объясняет мол мы решаем сложные задачи.
Вот, например, Hello, world HTTP сервер, из каждого первого spring-примера
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); server.createContext("/", e -> { var response = "Hello, world".getBytes(); e.sendResponseHeaders(200, response.length); e.getResponseBody().write(response); e.close(); }); server.start();
на полностью стандартной Java, без фреймворков и зависимостей. Я бы не сказал что прям супер сложно.
pin2t
В Spring-е и Java как всегда, придумали сотню невероятно сложных способов решить простейшую задачу - прочитать из файла настройки приложения.
Тонны аннотаций и конфигураций, фреймворков и библиотек, которые пересекаются между собой и работают каким-то магическим образом, или не работают.
И все это делает тоже самое что делает стандартный getProperty, без всяких фреймворков, аннотаций и конфигураций.
Kudesnik99 Автор
Вы правы, но это скорее философский вопрос подхода к разработке. Но, вообще-то, с @Value короче, чем у вас :)
pin2t
Где же короче-то, наоборот длиннее, потому что переменная всегда создается, вот с@value
+ 20 Мб зависимостей и медленная работа, т. к. там не умеют присваивать значения через =, там все через Reflection API
а у меня по-прежнему
без дополнительных затрат
AstarothAst
И вы по-прежнему просто прочитали строчку с диска. Вы не валидировали ее, вы не привели ее к нужному виду, вы не доставили ее до места применения, вы не научились ее обновлять на лету, вы ее даже не объединили в группу с другими свойствами того же вида. Вы просто прочитали строчку с диска, да еще завязавшись на конкретный файл свойств.
AstarothAst
Ваш вариант просто загрузил свойство. Спринг со всеми его наворотами позволяет валидировать, возвращать дефолты, если свойство не найдено, и еще много чего, вплоть до измененя свойства в рантайме, при чем по сети. Эти "невероятно сложные" (нет) возможности стоят того, что бы их освоить, как мне кажется.