В статье представлен пример локального окружения, построенного с использованием технологий: Docker Compose, Consul, Make — для разработки Spring Boot веб-сервисов.
Демонстрационное приложение будет по ссылке на страницу возвращать ссылку на наибольшее по размеру изображение с указанной страницы. Изображения будут извлекаться через Browserless, а PostgreSQL будет хранилищем результатов.
План:
- 1. Настройка сервисов в Docker Compose
- 2. Регистрация сервисов в Consul
- 3. Добавление значений в хранилище Consul
- 4. Создание Makefile
- 5. Инициализация проекта
- 6. Обнаружение PostgreSQL через Consul
- 7. Взаимодействие с Browserless через FeignClient
- 8. Контроллер и сервис
- 9. Заключение
1. Настройка сервисов в Docker Compose
Docker Compose — это инструмент для описания и запуска набора Docker-контейнеров. Настройка Docker Compose осуществляется в файле docker-compose.yml, который использует формат представления данных YAML.
Необходимо его создать — touch docker-compose.yml. В данном случае в файле достаточно двух секций:
- версия конфигурации
version, - настройки контейнеров с сервисами
services.
В секции services описаны три сервиса: Consul, PostgreSQL, Browserless. Для каждого сервиса указывается образ контейнера image, имя хоста hostname, соответствие портов внутри контейнера портам хостовой машины ports.
PostgreSQL требует установки нескольких переменных среды: POSTGRES_DB — имя БД, POSTGRES_PASSWORD — пароль для подключения к базе данных пользователя по умолчанию postgres. Переменные среды часто используются для конфигурации сервисов внутри контейнера.
version: '3.4' services: consul: image: consul:1.1.0 hostname: localhost ports: - 8500:8500 postgres: image: postgres:11.0 hostname: localhost ports: - 5432:5432 environment: POSTGRES_PASSWORD: example_pass POSTGRES_DB: example_app browserless: image: browserless/chrome hostname: localhost ports: - 3000:3000
Чтобы запустить сервисы необходимо в директории с docker-compose.yml выполнить команду docker-compose up. При первом запуске будут загружены необходимые образы контейнеров, поэтому выполнение команды может занять некоторое время.
После запуска всех контейнеров необходимо в браузере загрузить страницу веб-клиента Consul по адресу localhost:8500. На данном этапе среди зарегистрированных сервисов будет отображаться только Consul:

Чтобы остановить контейнеры можно воспользоваться командой docker-compose down.
Дополнительную информацию по Docker Compose можно найти здесь.
2. Регистрация сервисов в Consul
Для обнаружения сервисов в приложении через Consul их нужно зарегистрировать. Регистрацию сервиса можно выполнить через HTTP API Consul, отправив POST-запрос на http://localhost:8500/v1/agent/service/register. Тело запроса должно содержать описание сервиса в виде JSON-объекта с полями:
Name,ID,Tags— имя, идентификатор, теги сервиса;Address,Port— имя хоста и порт, на которых запущен сервис;Check— данные для проверки статуса сервиса.
Значением поля Check является JSON-объект с полями:
Name,ID— имя и идентификатор проверки;Interval— интервал опроса сервиса;Timeout— таймаут запроса к сервису;TCPилиHTTP— конечная точка для проверки статуса сервиса, в зависимости от используемого протокола используются разные поля;Status— необходимый статус, для успешного прохождения проверки.
Удобно все запросы к Consul выполнять через curl и сложить их в один скрипт.
#!/bin/bash curl -s -XPUT -d"{ \"Name\": \"postgres\", \"ID\": \"postgres\", \"Tags\": [ \"postgres\" ], \"Address\": \"localhost\", \"Port\": 5432, \"Check\": { \"Name\": \"PostgreSQL TCP on port 5432\", \"ID\": \"postgres\", \"Interval\": \"10s\", \"TCP\": \"postgres:5432\", \"Timeout\": \"1s\", \"Status\": \"passing\" } }" localhost:8500/v1/agent/service/register curl -s -XPUT -d"{ \"Name\": \"browserless\", \"ID\": \"browserless\", \"Tags\": [ \"browserless\" ], \"Address\": \"localhost\", \"Port\": 3000, \"Check\": { \"Name\": \"Browserless TCP on port 3000\", \"ID\": \"browserless\", \"Interval\": \"10s\", \"TCP\": \"browserless:3000\", \"Timeout\": \"1s\", \"Status\": \"passing\" } }" localhost:8500/v1/agent/service/register
Необходимо обратить внимание на то, что изначально файл скрипта не является исполняемым, если это не задано при создании. Чтобы сделать его исполняемым нужно выполнить chmod +x register-services.sh.
После выполнения скрипта register-services.sh в списке зарегистрированных сервисов в Consul появятся PostgreSQL и Browserless:

3. Добавление значений в хранилище Consul
Consul предоставляет key-value хранилище, которое можно использовать для хранения конфигураций приложения. В данном случае в нём будут храниться параметры для подключения к БД: имя БД, пользователь и пароль.
В реальном приложении для хранения секретных значений лучше использовать Vault — так же, как и Consul, разработан компанией HashiCorp и представляет собой хранилище секретных значений: паролей, ключей и т. д.
Для добавления значения в хранилище необходимо отправить PUT-запрос на http://localhost:8500/v1/kv/some/your/property, где some/your/property — это путь к значению. Тело запроса должно содержать само значение.
Необходимые приложению значения можно добавить командами:
# Добавление имени БД curl --request PUT --data example_app localhost:8500/v1/kv/example.app/db/name # Добавление имени пользователя curl --request PUT --data postgres localhost:8500/v1/kv/example.app/db/username # Добавление пароля curl --request PUT --data example_pass localhost:8500/v1/kv/example.app/db/password
Опция --data используется для установки значения тела запроса.
Добавленные в хранилище значения можно найти на вкладке Key/Value веб-клиента Consul:

Команды удобно поместить в отдельный скрипт, а при наличии большего числа значений лучше написать загрузчик из YAML или JSON файла.
4. Создание Makefile
Внимание: Данный раздел скорее для пользователей Linux или MacOS, т. к. make изначально недоступен на Windows. Однако прочитать его стоит, потому что здесь описаны используемые далее команды для работы с проектом.
Make — это инструмент, позволяющий собрать команды для управления проектом в одно место и позволяющий абстрагироваться от инструментов языка разработки, например, от Maven или Gradle для Java или от yarn или npm для JavaScript.
Makefile — файл, содержащий все команды проекта, с его форматом можно ознакомиться здесь.
В случае демонстрационного проекта можно добавить команды для запуска контейнеров docker_up, инициализации Consul consul_up, компиляции compile и запуска run приложения и команду для запуска всего в одну строку up, для остановки окружения — команду down, которая останавливает и удаляет контейнеры.
docker_up: @docker-compose up -d consul_up: @./register-services.sh && \ ./register-variables.sh compile: @cd example.app && mvn package run: @cd example.app && java -jar target/example.app-1.0-SNAPSHOT.jar up: docker_up consul_up compile run down: @docker-compose down
Внимание: в Makefile для отступов используется tab.
5. Инициализация проекта
Дальнейшая работа требует Spring Boot Maven проект, который можно создать в генераторе.
org.springframework.boot:
spring-boot-starter— основной модуль Spring Boot;spring-boot-starter-data-jpa— работа с хранилищами, например, PostgreSQL;spring-boot-starter-web— веб-модуль;spring-boot-starter-actuator— метрики, мониторинг приложения и т. д.
org.springframework.cloud:
spring-cloud-starter-consul-config— использование Consul как источник конфигураций;spring-cloud-starter-consul-discovery— обнаружение сервисов через Consul;spring-cloud-starter-openfeign— FeignClient.
org.postgresql:postgresql — клиентский модуль для работы с PostgreSQL.
Перед началом работы также нужно выполнить начальную конфигурацию.
Необходимо создать главный класс приложения с методом main. Класс должен быть помечен аннотациями SpringBootApplication и EnableFeignClients.
@SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Также необходимо указать некоторые свойства для Consul, FeignClient и Spring JPA в bootstrap.properties и application.yml:
Настройка Consul и Spring JPA.
spring.application.name=example_app spring.cloud.consul.host=localhost spring.cloud.consul.port=8500 spring.cloud.consul.config.enabled=true spring.cloud.consul.config.prefix= spring.cloud.consul.config.defaultContext=example.app spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false spring.jpa.hibernate.ddl-auto=update
Установка таймаутов для запросов через FeignClient.
feign: client: config: default: connectTimeout: 20000 readTimeout: 20000
6. Обнаружение PostgreSQL через Consul
Для работы с PostgreSQL необходимо написать конфигурацию, которая представляет собой инициализированный объект класса DataSource. Его инициализация требует некоторых значений, которые можно получить из Consul: имя БД databaseName, пользователь databaseUsername и пароль databasePassword — будут загружены из key-value хранилища Consul, а также хост и порт, на которых запущен PostgreSQL — будут получены из данных о зарегистрированных сервисах в Consul.
@Configuration public class PersistenceConfiguration { @Value("${example.app.db.name}") private String databaseName; @Value("${example.app.db.username}") private String databaseUsername; @Value("${example.app.db.password}") private String databasePassword; @Autowired private DiscoveryClient discoveryClient; @Bean @Primary public DataSource dataSource() { var postgresInstance = getPostgresInstance(); return DataSourceBuilder .create() .username(databaseUsername) .password(databasePassword) .url(format("jdbc:postgresql://%s:%s/%s", postgresInstance.getHost(), postgresInstance.getPort(), databaseName)) .driverClassName("org.postgresql.Driver") .build(); } private ServiceInstance getPostgresInstance() { return discoveryClient.getInstances("postgres") .stream() .findFirst() .orElseThrow(() -> new IllegalStateException("Unable to discover a Postgres instance")); } }
Метод getPostgresInstance(), используя DiscoveryClient, находит зарегистрированные в Consul сервисы с тегом postgres. Обнаруженные сервисы представляются объектами типа ServiceInstance, в котором содержатся данные о конкретном сервисе, включая его хост и порт.
Далее необходимо создать репозиторий для сущности Image, который предоставит методы для сохранения, обновления, удаления, выборки объектов данного типа:
@Repository public interface ImageRepository extends JpaRepository<Image, Long> {}
Класс Image содержит генерируемый PostgreSQL числовой идентификатор и две ссылки: на страницу и на обнаруженное на ней изображение:
@Entity @Table(name = "images") public class Image { @Id @GeneratedValue private Long id; @NotBlank private String sourceUrl; @NotBlank private String imageUrl; // getters and setters }
7. Взаимодействие с Browserless через FeignClient
Browserless предоставляет HTTP API, для взаимодействия с которым необходимо сгенерировать клиент с использованием FeignCleint.
Нужно создать интерфейс и пометить его аннотацией FeignClient, единственный параметр аннотации — это имя, под которым зарегистрирован Browserless в Consul. В интерфейсе должен быть метод, помеченный аннотациями аналогичными для конечных точек в контроллерах, данный метод будет преобразован в HTTP запрос с параметрами, заданными в аннотациях.
В данном случае достаточно аннотации PostMapping, которая означает отправку POST-запроса по заданному пути (хост и порт получаются из Consul автоматически), и аннотации RequestBody, которая означает, что параметр метода будет значением тела запроса. Тип возвращаемого значения метода представляет собой класс, в объект которого отобразится тело ответа. По умолчанию тело запроса и ответа содержит JSON строку.
@FeignClient("browserless") public interface BrowserlessClient { @PostMapping("/function") LargestImageResponse findLargestImage(@RequestBody LargestImageRequest request); class LargestImageResponse { private String url; // getters and setters } class LargestImageRequest { private String code; private LargestImageRequestPayload context; // constructor, getters and setters } class LargestImageRequestPayload { private String url; // constructor, getters and setters } }
В ресурсы необходимо положить JS-скрипт, который вытаскивает наибольшее изображение со страницы. Содержимое скрипта будет передаваться в Browserless в каждом запросе, как LargestImageRequest.code.
Внимание: Объяснение содержимого скрипта находится вне рамок данной статьи.
module.exports = async ({page, context}) => { const {url} = context; await page.goto(url); await page.evaluate(_ => { window.scrollBy(0, window.innerHeight); }); const data = await page._client.send('Page.getResourceTree') .then(tree => { return Array.from(tree.frameTree.resources) .filter(resource => resource.type === 'Image' && resource.url && resource.url.indexOf('.svg') == -1) .sort((a, b) => b.contentSize - a.contentSize)[0]; }); return { data, type: 'json' }; };
8. Контроллер и сервис
Сервис предоставляет метод, в котором происходит взаимодействие с репозиторием изображений и клиентом Browserless при запросе от клиента.
@Service public class ImageService { @Autowired private ImageRepository imageRepository; @Autowired private BrowserlessClient browserlessClient; private String getLargestImageScript; @PostConstruct public void initialize() throws IOException { getLargestImageScript = IOUtils.toString(getClass().getResourceAsStream("/getLargestImage.js"), StandardCharsets.UTF_8.name()); } public Image findLargestImage(String url) { var largestImageResponse = browserlessClient.findLargestImage( new LargestImageRequest( getLargestImageScript, new LargestImageRequestPayload(url))); var image = new Image(); image.setSourceUrl(url); image.setImageUrl(largestImageResponse.getUrl()); return imageRepository.save(image); } }
Контроллер предоставляет HTTP точку для запроса на получение картинки по ссылке на страницу.
@RestController public class ImageController { @Autowired private ImageService imageService; @GetMapping("/largest-image") public ResponseEntity<Image> getTitle(@RequestParam("url") String url) { return ResponseEntity.ok(imageService.findLargestImage(url)); } }
9. Заключение
Для тестирования работы приложения можно отправить GET-запрос по адресу http://localhost:8888/largest-image?url=https://maximka777.github.io/mb:

В итоге:
- необходимые сервисы загружаются через Docker Compose и регистрируются в Consul для их последующего обнаружения;
- конфигурация приложения загружается в key-value хранилище Consul;
- Makefile содержит все команды по управлению проектом;
- в приложении конфигурация для подключения к PostgreSQL загружается из key-value хранилища Consul, также из Consul подтягиваются данные о PostgreSQL сервисе;
- FeignClient позволяет сгенерировать клиент для работы с Browserless.
Спасибо за внимание!
Весь код к проекту можно найти здесь.
