- PVSM.RU - https://www.pvsm.ru -
[1] Всем привет! В основном данная книга предназначена для разработчиков Java- и JVM-машин, которые ищут способы создания более качественного ПО в короткие сроки с помощью Spring Boot, Spring Cloud и Cloud Foundry. Она для тех, кто уже слышал шум, поднявшийся вокруг микросервисов. Возможно, вы уже поняли, на какую стратосферную высоту взлетела среда Spring Boot, и удивляетесь тому, что сегодня предприятия используют платформу Cloud Foundry. Если так и есть, то эта книга для вас.
В этой главе будет рассмотрен порядок реализации конфигурации приложения.
Определим ряд словарных терминов. Когда речь заходит о конфигурации в Spring, чаще всего имеется в виду ввод в среду Spring различных реализаций контекста приложения — ApplicationContext [2], что помогает контейнеру понять, как связать bean-компоненты. Такую конфигурацию можно представить в виде файла в формате XML, который должен быть подан в ClassPathXmlApplicationContext [3], или классов Java, аннотированных способом, позволяющим быть предоставленными объекту AnnotationConfigApplicationContext [4]. И конечно же, при изучении последнего варианта мы станем ссылаться на конфигурацию Java.
Но в этой главе мы собираемся рассмотреть конфигурацию в том виде, в котором она определена в манифесте 12-факторного приложения [5]. В данном случае она касается буквальных значений, способных изменяться от одной среды окружения к другой: речь идет о паролях, портах и именах хостов или же о флагах свойств. Конфигурация игнорирует встроенные в код магические константы. В манифест включен отличный критерий правильности настройки конфигурации: может ли кодовая база приложения быть открытым источником в любой момент без раскрытия и компрометации важных учетных данных? Эта разновидность конфигурации относится исключительно к тем значениям, которые изменяются от одной среды окружения к другой, и не относится, например, к подключению bean-компонентов Spring или конфигурации маршрутов Ruby.
В Spring стиль конфигурации, соответствующий 12 факторам, поддерживается с появления класса PropertyPlaceholderConfigurer [6]. Как только определяется его экземпляр, он заменяет литералы в XML-конфигурации значениями, извлеченными из файла с расширением .properties. В среде Spring класс PropertyPlaceholderConfigurer [7] предлагается с 2003 года. В Spring 2.5 появилась поддержка пространства имен XML, а вместе с тем и поддержка в данном пространстве подстановки свойств. Это позволяет проводить подстановку в XML-конфигурации литеральных значений определений bean-компонентов значениями, назначенными ключам во внешнем файле свойств (в данном случае в файле simple.properties, который может фигурировать в пути к классам или быть внешним по отношению к приложению).
Конфигурация в стиле 12 факторов нацелена на устранение ненадежности имеющихся магических строк, то есть значений наподобие адресов баз данных и учетных записей для подключения к ним, портов и т.д., жестко заданных в скомпилированном приложении. Если конфигурация вынесена за пределы приложения, то ее можно заменить, не прибегая для этого к новой сборке кода.
Посмотрим образец использования класса PropertyPlaceholderConfigurer, XML-определений bean-компонентов Spring и вынесенного за пределы приложения файла с расширением .properties. Нам нужно просто вывести значение, имеющееся в данном файле свойств. Это поможет сделать код, показанный в примере 3.1.
Пример 3.1. Файл свойств: some.properties
configuration.projectName=Spring Framework
Это принадлежащий Spring класс ClassPathXmlApplicationContext, таким образом, мы используем пространство имен XML из контекста Spring и указываем на наш файл some.properties. Затем в определениях bean-компонентов задействуем литералы в форме ${configuration.projectName}, и Spring в ходе выполнения заменит их значениями из нашего файла свойств (пример 3.2).
Пример 3.2. XML-файл Spring-конфигурации
<context:property-placeholder location="classpath:some.properties"/> (1)
<bean class="classic.Application">
<property name="configurationProjectName"
value="${configuration.projectName}"/>
</bean>
(1) classpath: местоположение, ссылающееся на файл в текущем откомпилированном блоке кода (.jar, .war и т. д.). Spring поддерживает множество альтернативных вариантов, включая file: и url:, позволяющих файлу существовать обособленно от блока кода.
И наконец, рассмотрим, как выглядит класс Java, благодаря которому возможно свести все это воедино (пример 3.3).
Пример 3.3. Класс Java, который должен быть сконфигурирован со значением свойства
package classic;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Application {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("classic.xml");
}
public void setConfigurationProjectName(String pn) {
LogFactory.getLog(getClass()).info("the configuration project name is " + pn);
}
}
В первом примере используется XML-формат конфигурации bean-компонентов Spring. В Spring 3.0 и 3.1 ситуация для разработчиков, применяющих конфигурацию Java, значительно улучшилась. В этих выпусках были введены аннотация Value [8] и абстракция Environment.
Абстракция Environment [9] представляет в ходе выполнения кода его косвенное отношение к той среде окружения, в которой он запущен, и позволяет приложению ставить вопрос («Какой разделитель строк line.separator на данной платформе?») о свойствах среды. Абстракция действует в качестве отображения из ключей и значений. Сконфигурировав в Environment источник свойств PropertySource, можно настроить то место, откуда эти значения будут считываться. По умолчанию Spring загружает системные ключи и значения среды, такие как line.separator. Системе Spring можно предписать загрузку ключей конфигурации из файла в том же порядке, который мог бы использоваться в ранних выпусках решения Spring по подстановке свойств с помощью аннотации @PropertySource.
Аннотация Value [8] предоставляет способ внедрения значений среды окружения в конструкторы, сеттеры, поля и т. д. Эти значения могут быть вычислены с помощью языка выражений Spring Expression Language или синтаксиса подстановки свойств при условии регистрации PropertySourcesPlaceholderConfigurer [10], как сделано в примере 3.4.
Пример 3.4. Регистрация PropertySourcesPlaceholderConfigurer
package env;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
(1)
@Configuration
@PropertySource("some.properties")
public class Application {
private final Log log = LogFactory.getLog(getClass());
public static void main(String[] args) throws Throwable {
new AnnotationConfigApplicationContext(Application.class);
}
(2)
@Bean
static PropertySourcesPlaceholderConfigurer pspc() {
return new PropertySourcesPlaceholderConfigurer();
}
(3)
@Value("${configuration.projectName}")
private String fieldValue;
(4)
@Autowired
Application(@Value("${configuration.projectName}") String pn) {
log.info("Application constructor: " + pn);
}
(5)
@Value("${configuration.projectName}")
void setProjectName(String projectName) {
log.info("setProjectName: " + projectName);
}
(6)
@Autowired
void setEnvironment(Environment env) {
log.info("setEnvironment: " + env.getProperty("configuration.projectName"));
}
(7)
@Bean
InitializingBean both(Environment env,
@Value("${configuration.projectName}") String projectName) {
return () -> {
log.info("@Bean with both dependencies (projectName): " + projectName);
log.info("@Bean with both dependencies (env): "
+ env.getProperty("configuration.projectName"));
};
}
@PostConstruct
void afterPropertiesSet() throws Throwable {
log.info("fieldValue: " + this.fieldValue);
}
}
(1) Аннотация @PropertySource — сокращение наподобие property-placeholder, настраивающее PropertySource из файла с расширением .properties.
(2) PropertySourcesPlaceholderConfigurer нужно зарегистрировать в качестве статического bean-компонента, поскольку он является реализацией BeanFactoryPostProcessor и должен вызываться на ранней стадии жизненного цикла инициализации в Spring bean-компонентов. При использовании в Spring XML-конфигурации bean-компонентов этот нюанс не просматривается.
(3) Можно отдекорировать поля аннотацией Value [8] (но не делайте этого, иначе код не пройдет тестирование!)…
(4) …или аннотацией Value [8] можно отдекорировать параметры конструктора…
(5) …или воспользоваться методами установки…
(6) …или внедрить объект Spring Environment и выполнить разрешение ключа вручную.
(7) Параметры с аннотацией Value [8] можно использовать также в поставщике аргументов методов Bean [11] в Java-конфигурации Spring.
В этом примере значения загружаются из файла simple.properties, а затем в нем имеется значение configuration.projectName, предоставляемое различными способами.
Кроме всего прочего, абстракция Environment вводит профили [12]. Это позволяет приписывать метки (профили) в целях группировки bean-компонентов. Профили следует использовать для описания bean-компонентов и bean-графов, изменяющихся от среды к среде. Одновременно могут активироваться сразу несколько профилей. Bean-компоненты, не имеющие назначенных им профилей, активируются всегда. Bean-компоненты, имеющие профиль default, активируются только в том случае, если у них нет других активных профилей. Атрибут profile можно указать в определении bean-компонента в XML либо в классах тегов, классах конфигурации, отдельно взятых bean-компонентах или в методах Bean [11]-поставщика с помощью Profile [13].
Профили позволяют описывать наборы bean-компонентов, которые должны быть созданы в одной среде несколько иначе, чем в другой. В локальном разработочном dev-профиле можно, к примеру, воспользоваться встроенным источником данных H2 javax.sql.DataSource, а затем, когда активен prod-профиль, переключиться на источник данных javax.sql.DataSource для PostgreSQL, получаемый с помощью JNDI-поиска или путем чтения свойств из переменной среды в Cloud Foundry [14]. В обоих случаях ваш код будет работоспособен: вы получаете javax.sql.DataSource, но решение о том, какой конкретный экземпляр задействовать, принимается с помощью активации одного профиля или нескольких (пример 3.5).
Пример 3.5. Демонстрация того, что классы @Configuration могут загружать различные файлы конфигурации и предоставлять различные bean-компоненты на основе
активного профиля
package profiles;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
@Configuration
public class Application {
private Log log = LogFactory.getLog(getClass());
@Bean
static PropertySourcesPlaceholderConfigurer pspc() {
return new PropertySourcesPlaceholderConfigurer();
}
(1)
@Configuration
@Profile("prod")
@PropertySource("some-prod.properties")
public static class ProdConfiguration {
@Bean
InitializingBean init() {
return () -> LogFactory.getLog(getClass()).info("prod InitializingBean");
}
}
@Configuration
@Profile({ "default", "dev" })
(2)
@PropertySource("some.properties")
public static class DefaultConfiguration {
@Bean
InitializingBean init() {
return () -> LogFactory.getLog(getClass()).info("default InitializingBean");
}
}
(3)
@Bean
InitializingBean which(Environment e,
@Value("${configuration.projectName}") String
projectName) {
return () -> {
log.info("activeProfiles: '"
+ StringUtils.arrayToCommaDelimitedString(e.getActiveProfiles()) +
"'");
log.info("configuration.projectName: " + projectName);
};
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext();
ac.getEnvironment().setActiveProfiles("dev"); (4)
ac.register(Application.class);
ac.refresh();
}
}
(1) Этот класс конфигурации и все имеющиеся в нем определения Bean [11] будут вычислены только в случае активности prod-профиля.
(2) Данный класс конфигурации и все имеющиеся в нем определения Bean [11] будут вычислены, только если активен dev-профиль или не активен ни один профиль, включая dev.
(3) Этот компонент InitializingBean просто записывает текущий активный профиль и вводит значение, которое в конечном итоге было внесено в файл свойств.
(4) Активировать профиль (или профили) программным способом довольно просто.
Spring откликается еще на несколько других методов активации профилей, использующих токен spring_profiles_active или spring.profiles.active. Профиль можно установить с помощью переменной среды (например, SPRING_PROFILES_ACTIVE), JVM-свойства (‑Dspring.profiles.active=… ), параметра инициализации сервлет-приложения или программным способом.
Spring Boot [15] существенно улучшает ситуацию. Среда изначально автоматически загрузит свойства из иерархии заранее известных мест. Аргументы командной строки переопределяют значения свойств, полученных из JNDI, которые переопределяют свойства, полученные из System.getProperties(), и т. д.
— Аргументы командной строки.
— Атрибуты JNDI из java:comp/env.
— Свойства System.getProperties().
— Переменные среды операционной системы.
— Внешние файлы свойств в файловой системе: (config/)?application.(yml.properties).
— Внутренние файлы свойств в архиве (config/)?application.(yml.properties).
— Аннотация @PropertySource в классах конфигурации.
— Исходные свойства из SpringApplication.getDefaultProperties().
В случае активности профиля [16] будут автоматически считаны данные из файлов конфигурации, основанных на имени профиля, например из такого файла, как src/main/resources/application-foo.properties, где foo — текущий профиль.
Если библиотека SnakeYAML [17] упомянута в путях к классам (classpath), то будут также автоматически загружены YAML-файлы, следуя в основном тому же соглашению.
На странице спецификации YAML [18] указано, что «YAML является удобным для человеческого восприятия стандартом сериализации данных для всех языков программирования». YAML — иерархическое представление значений. В обычных файлах с расширением .properties иерархия обозначается с помощью точки («.»), а в YAML-файлах используется символ новой строки и дополнительный уровень отступа. Было бы неплохо воспользоваться этими файлами, чтобы избежать необходимости указывать общие корни при наличии сильно разветвленных деревьев конфигурации.
Содержимое файла с расширением .yml показано в примере 3.6.
Пример 3.6. Файл свойств application.yml. Данные изложены в иерархическом порядке
configuration:
projectName : Spring Boot
management:
security:
enabled: false
Кроме того, среда Spring Boot существенно упрощает получение правильного результата в общих случаях. Она превращает аргументы -D в переменные процесса и среды java, доступные в качестве свойств. Она даже проводит их нормализацию, при которой переменная среды $CONFIGURATION_PROJECTNAME (КОНФИГУРАЦИЯ_ИМЯПРОЕКТА) или аргумент -D в форме ‑Dconfiguration.projectName (конфигурация.имя_проекта) становятся доступными с помощью ключа configuration.projectName (конфигурация.имя_проекта) точно так же, как ранее был доступен токен spring_profiles_active.
Значения конфигурации являются строками и при их достаточном количестве могут стать неудобочитаемыми при попытке убедиться, что такие ключи не стали сами по себе магическими строками в коде. В Spring Boot вводится тип компонента @ConfigurationProperties. При аннотировании POJO — Plain Old Java Object (обычный старый объект Java) — с помощью @ConfigurationProperties и указании префикса среда Spring предпримет попытку отобразить все свойства, начинающиеся с этого префикса, на POJO-свойства. В показанном ниже примере значение для configuration.projectName будет отображено на экземпляр POJO, который весь код затем может внедрить и разыменовать для типобезопасного чтения значений. Как следствие, у вас будет только отображение из (String) ключа в одном месте (пример 3.7).
Пример 3.7. Автоматическое разрешение свойств из src/main/resources/application.yml
package boot;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
(1)
@EnableConfigurationProperties
@SpringBootApplication
public class Application {
private final Log log = LogFactory.getLog(getClass());
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Autowired
public Application(ConfigurationProjectProperties cp) {
log.info("configurationProjectProperties.projectName = "
+ cp.getProjectName());
}
}
(2)
@Component
@ConfigurationProperties("configuration")
class ConfigurationProjectProperties {
private String projectName; (3)
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
}
(1) Аннотация @EnableConfigurationProperties предписывает среде Spring отображать свойства на POJO-объекты, аннотированные с помощью @ConfigurationProperties.
(2) Аннотация @ConfigurationProperties показывает среде Spring, что этот bean-компонент должен использоваться как корневой для всех свойств, начинающихся с configuration., с последующими токенами, отображаемыми на свойства объекта.
(3) Поле projectName будет в конечном итоге иметь значение, присвоенное ключу свойства configuration.projectName.
Spring Boot активно применяет механизм @ConfigurationProperties, чтобы дать пользователям возможность переопределять элементарные составляющие системы. Можно заметить, что ключи свойств позволяют вносить изменения, например, путем добавления зависимости org.springframework.boot:spring-boot-starter-actuator в веб-приложение на основе Spring Boot с последующим посещением страницы 127.0.0.1 [19]:8080/configprops.
Конечные точки актуатора более подробно будут рассмотрены в главе 13. Они заперты и требуют по умолчанию имени пользователя и пароля. Меры безопасности можно отключить (но только чтобы взглянуть на эти точки), указав management.security.enabled=false в файле application.properties (или в application.yml).
Вы получите список поддерживаемых свойств конфигурации на основе типов, представленных в путях к классам (classpath) во время выполнения. По мере наращивания количества типов Spring Boot будут показываться дополнительные свойства. В этой конечной точке также станут отображаться свойства, экспортированные вашими POJO-объектами, имеющими аннотацию @ConfigurationProperties.
Пока все хорошо, однако нужно, чтобы дела шли еще успешнее. Мы по-прежнему не ответили на вопросы, касающиеся общих случаев применения:
Проблему централизации конфигурации можно решить, сохранив конфигурацию в одном каталоге и указав всем приложениям на него. Можно также установить управление версиями этого каталога, используя Git или Subversion. Тогда будет получена поддержка, необходимая для проверки и регистрирования. Но последние два требования по-прежнему не будут выполнены, поэтому нужно нечто более изощренное. Обратимся к серверу конфигурации Spring Cloud [20]. Платформа Spring Cloud предлагает сервер конфигурации и клиента для этого сервера.
Сервер Spring Cloud Config представляет собой REST API, к которому будут подключаться наши клиенты, чтобы забирать свою конфигурацию. Сервер также управляет хранилищем конфигураций с управлением версиями. Он посредник между нашими клиентами и хранилищем конфигурации и таким образом находится в выгодной позиции, позволяющей внедрять средства обеспечения безопасности подключений со стороны клиентов к сервису и подключений со стороны сервиса к хранилищу конфигураций с управлением версиями. Клиент Spring Cloud Config предоставляет клиентским приложениям новую область видимости, refresh, дающую возможность конфигурировать компоненты Spring заново, не прибегая к перезапуску приложения.
Технологии, подобные серверу Spring Cloud Config, играют важную роль, но влекут дополнительные рабочие издержки. В идеале эта обязанность должна быть переложена на платформу и автоматизирована. При использовании Cloud Foundry в каталоге сервисов можно найти сервис Config Server, действия которого основаны на применении сервера Spring Cloud Config.
Рассмотрим простой пример. Сначала настроим сервер Spring Cloud Config. К одному такому сервису могут иметь доступ сразу несколько приложений Spring Boot. Вам нужно где-то и как-то заставить его функционировать. Затем останется только оповестить все наши сервисы о том, где найти сервис конфигурации. Он работает как некий посредник для ключей конфигурации и значений, которые он считывает из Git-хранилища по сети или с диска. Добавьте к сборке вашего приложения Spring Boot строку org.springframework.cloud: spring-cloud-config-server, чтобы ввести сервер Spring Cloud Config (пример 3.8).
Пример 3.8. Чтобы встроить в сборку сервер конфигурации, воспользуйтесь аннотацией @EnableConfigServer
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
(1)
@SpringBootApplication
@EnableConfigServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
(1) Использование аннотации @EnableConfigServer приводит к установке сервера Spring Cloud Config.
В примере 3.9 показана конфигурация для сервиса конфигурации.
Пример 3.9. Конфигурация сервера конфигурации src/main/resources/application.yml
server.port=8888
spring.cloud.config.server.git.uri=
github.com/cloud-native-java/config-server-configuration-repository [21] (1)
(1) Указание на работающее Git-хранилище, имеющее локальный характер либо доступное по сети (например, на GitHub (https://github.com/)) и используемое сервером Spring Cloud Config.
Здесь сервису конфигурации Spring Cloud предписывается выполнить поиск файлов конфигурации для отдельно взятых клиентов в Git-хранилище на GitHub. Мы указали на это хранилище, но подошла бы ссылка на любой действующий Git URI. Разумеется, он даже не обязан относится к Git-системе, можно воспользоваться Subversion или даже неуправляемыми каталогами (хотя мы настоятельно не рекомендуем делать это). В данном случае URI хранилища жестко задан, но нет ничего, что помешает получить его из аргумента -D, аргумента — или из переменной среды окружения.
» Более подробно с книгой можно ознакомиться на сайте издательства [22]
» Оглавление [23]
» Отрывок [24]
Для Хаброжителей скидка 20% по купону — Java
Автор: ph_piter
Источник [25]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/294606
Ссылки в тексте:
[1] Image: https://habr.com/company/piter/blog/425109/
[2] ApplicationContext: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/ApplicationContext.html
[3] ClassPathXmlApplicationContext: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html
[4] AnnotationConfigApplicationContext: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/annotation/AnnotationConfigApplicationContext.html
[5] 12-факторного приложения: https://12factor.net/config
[6] PropertyPlaceholderConfigurer: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.html
[7] PropertyPlaceholderConfigurer: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/config/PropertyPlaceholderConfigurer.html
[8] Value: https://habr.com/users/value/
[9] Environment: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/env/Environment.html
[10] PropertySourcesPlaceholderConfigurer: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.html
[11] Bean: https://habr.com/users/bean/
[12] профили: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html
[13] Profile: https://habr.com/users/profile/
[14] Cloud Foundry: https://cloudfoundry.org/
[15] Spring Boot: http://spring.io/projects/spring-boot
[16] активности профиля: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html
[17] SnakeYAML: https://bitbucket.org/asomov/snakeyaml
[18] YAML: http://yaml.org/
[19] 127.0.0.1: http://127.0.0.1
[20] Spring Cloud: http://cloud.spring.io/spring-cloud-config/
[21] github.com/cloud-native-java/config-server-configuration-repository: https://github.com/cloud-native-java/config-server-configuration-repository
[22] сайте издательства: https://www.piter.com/product/java-v-oblake-spring-boot-spring-cloud-cloud-foundry
[23] Оглавление: https://storage.piter.com/upload/contents/978544610713/978544610713_X.pdf
[24] Отрывок: https://storage.piter.com/upload/contents/978544610713/978544610713_p.pdf
[25] Источник: https://habr.com/post/425109/?utm_campaign=425109
Нажмите здесь для печати.