План:
- Настройка сервисов в Docker Compose
- Регистрация сервисов в Consul’e и добавление переменных в хранилище Consul’a
- Makefile
- Конфигурация БД
- FeignClient
- Конец
Данная статья показывает пример того, как поднять локальный development environment с использованием Docker Compose, Consul, Make для Spring Boot-приложения, использующего, например, PostgreSQL и Browserless.
Прилага абсолютно бесполезная: по ссылке возвращает ссылку на наибольшее по размеру изображение. Изображение будет извлекаться Browserless’ом, а в PostgreSQL это дело будет сохраняться.
1. Настройка сервисов в Docker Compose
Первое, что нужно сделать, это создать файл с конфигурацией docker-контейнеров docker-compose.yml
:
touch docker-compose.yml
Данный файл содержит версию docker-compose:
version: '3.4'
Конфигурацию сети:
networks:
lan:
И конфигурацию необходимых сервисов, в данном случае Consul, Browserless и PostgreSQL:
services:
consul:
image: consul:1.1.0
hostname: localhost
networks:
- lan
ports:
- 8500:8500
postgres:
image: postgres:11.0
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: example_app
browserless:
image: browserless/chrome
hostname: localhost
networks:
- lan
ports:
- 3000:3000
POSTGRES_PASSWORD
– пароль к базе данных дефолтного пользователя postgres
, POSTGRES_DB
– автоматически создаваемая база данных в контейнере.
Чтобы запустить сервисы необходимо выполнить команду:
docker-compose up
Ждем окончания загрузки образов контейнеров и запуска контейнеров. Чтобы остановить работу контейнеров используется команда docker-compose down
. После запуска всех контейнеров можно перейти по адресу в браузере localhost:8500
– должен открыться веб-клиент Consul’a (рис. 1).
Рисунок 1
2. Регистрация сервисов в Consul’e и добавление переменных в хранилище Consul’a
Регистрацию сервисов в Consul’e можно провести отправив несколько post-запросов на адрес localhost:8500/v1/agent/service/register
через curl (что угодно, на рабочем проекте я написал js-скрипт для этого).
Занесем все вызовы curl в bash-скрипт.
#!/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
curl -s -XPUT -d"{
"Name": "example.app",
"ID": "example.app",
"Tags": [ "example.app" ],
"Address": "localhost",
"Port": 8080,
"Check": {
"Name": "example.app HTTP on port 8080",
"ID": "example.app",
"Interval": "10s",
"HTTP": "example.app:8080/actuator/health",
"Timeout": "1s",
"Status": "passing"
}
}" localhost:8500/v1/agent/service/register
chmod +x register-services.sh
– чтобы сделать файл запускаемым.
После запуска скрипта в списке сервисов в Consule’e появятся только что зарегистрированные сервисы (рис. 2).
Рисунок 2
На рисунке видно, что проверка здоровья PostgreSQL не проходит – ничего страшного (на суть не влияет) .
Добавим конфигурацию в key/value-хранилище Consul’a. Создадим переменную test.property
в директории example.app
:
curl --request PUT --data TEST localhost:8500/v1/kv/example.app/test.property
Это тоже лучше сохранить в bash-скрипт.
3. Makefile
Для упрощения запуска всего этого напишем Makefile`:
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
down:
@docker-compose down
Warning: В Makefile
используется особый тип отступов!
Команда make up
запустит всю среду.
4. Конфигурация БД
Далее я сгенерировал базовый Spring Boot-проект (Maven) с использованием инициализатора Spring Boot-приложений https://start.spring.io/.
В pom.xml
были добавлены следующие зависимости:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Из названий пакетов понятно для чего они нужны.
Напишем конфигурацию для DataSource’a. В файл bootstrap.properties
вкинем конфиги:
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.cloud.consul.discovery.register=false
spring.cloud.service-registry.auto-registration.enabled=false
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=create
В application.yml
:
example.app:
db:
name: 'example_app'
feign:
client:
config:
default:
connectTimeout: 20000
readTimeout: 20000
loggerLevel: basic
management:
endpoint:
health:
show-details: always
endpoints:
web.exposure:
include: '*'
И сам класс конфигурации:
@Configuration
public class PersistenceConfiguration {
@Value("${example.app.db.name}")
private String databaseName;
@Autowired
private DiscoveryClient discoveryClient;
@Bean
@Primary
public DataSource dataSource() {
var postgresInstance = getPostgresInstance();
return DataSourceBuilder
.create()
.username("postgres")
.password("password")
.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()
берет первый инстанс сервиса с тэгом postgres
, зарегистрированный в Consul’e. Метод dataSource()
– бин DataSource’a.
Далее объявим репозиторий с базовыми операциями над сущностью Image
, которая хранит исходный адрес и адрес изображения, найденного по исходному:
@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {
}
5. FeignClient
Далее в ресурсы закинем JS-скрипт, который будет вытаскивать наибольшее изображение со страницы.
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'
};
};
Определим интерфейс BlowserlessClient:
@FeignClient("browserless") // по тэгу достанет адрес из консула
public interface BrowserlessClient {
@PostMapping("/function")
ImageInfo findLargestImage(LargestImageRequest request);
// дата класс результата выполнения запроса к Browserless’у со ссылкой на изображение
class ImageInfo {
private String url;
public String getUrl() {
return url;
}
}
// хранит скрипт, который передается в Browserless на выполнение, и объект запроса
class LargestImageRequest {
private String code;
private BrowserlessContext context;
public LargestImageRequest(String code, BrowserlessContext context) {
this.code = code;
this.context = context;
}
public String getCode() {
return code;
}
public BrowserlessContext getContext() {
return context;
}
}
// дата класс запроса со ссылкой на страницу
class BrowserlessContext {
private String url;
public BrowserlessContext(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
}
}
Метод сервиса, запрашивающий изображение и сохраняющий в БД:
public Image findLargestImage(String url) {
var browserlessContext = new BrowserlessContext(url);
var largestImageRequest = new LargestImageRequest(getLargestImageScript, browserlessContext);
var imageInfo = browserlessClient.findLargestImage(largestImageRequest);
var image = new Image();
image.setSourceUrl(url);
image.setImageUrl(imageInfo.getUrl());
return imageRepository.save(image);
}
Контроллер для проверки функциональности:
public class MainController {
private static Logger log = LoggerFactory.getLogger(MainController.class);
@Autowired
private ImageService imageService;
@Value("${test.property}")
private String testProperty;
@GetMapping("/largest-image")
public ResponseEntity<Image> getTitle(@RequestParam("url") String url) {
return ResponseEntity.ok(imageService.findLargestImage(url));
}
@GetMapping("/property")
public ResponseEntity<String> getProperty() {
return ResponseEntity.ok(testProperty);
}
}
Здесь поле testProperty
вытягивается из key/value-хранилища Consul’a.
6. Конец
Всё !
Надеюсь, я смог показать вариант возможной конфигурации представленных инструментов и данная статья будет кому-то полезной.
В данное статье не очень много пояснений, так как я считаю, что здесь легче по коду разобраться с минимальными комментариями.
Автор: maxim147