В статье представлен пример локального окружения, построенного с использованием технологий: 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.
Спасибо за внимание!
Весь код к проекту можно найти здесь.