Docker Compose + Consul + Spring Boot + FeignClient

в 11:56, , рубрики: browserless, consul, docker-compose, feignclient, java, make, postgresql, spring boot

План:

  1. Настройка сервисов в Docker Compose
  2. Регистрация сервисов в Consul’e и добавление переменных в хранилище Consul’a
  3. Makefile
  4. Конфигурация БД
  5. FeignClient
  6. Конец

Данная статья показывает пример того, как поднять локальный 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

Рисунок 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

Рисунок 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js