null

Реактивное приложение на Spring Boot и MongoDB

Исходные данные

Java 8 и выше

Spring Boot 2.1.2 и выше

Spring-boot-starter-webflux

Spring-boot-starter-data-mongodb-reactive

Lombok

MongoDB (например flapdoodle.embed.mongo)

Введение

Реактивные приложения (отзывчивые, Reactive) – это приложения, позволяющие добиться большей производительности и масштабируемости по сравнению с обычными приложениями, когда имеет место взаимодействие с большим количеством потоковых данных.

В основе реактивного приложения лежит асинхронный ввод-вывод. Таким образом приложение получается неблокирующим (non blocking) и эффективно использующим вычислительные ресурсы. В противоположность синхронному выполнению программы, когда «клиент» запрашивает данные и блокируется до тех пор, пока он эти данные не получит (тем самым ресурсы простаиваются), асинхронное выполнение подразумевает обратную стратегию, - клиент подписывается на получение данных и продолжает свою работу. Когда данные готовы, клиент получает уведомление и может приступить к получению и обработке этих данных.

Для того, чтобы реактивное приложение полностью раскрыло свой потенциал, необходимо чтобы оно было отзывчивым на всех своих уровнях, включая базу данных.

К примеру, использование блокирующего JDBC драйвера очень сильно влияет на снижение производительности приложения, в то время как применение отзывчивых NoSQL баз данных, таких как Cassandra, MongoDB, Couchbase или Redis позволяет добиться превосходной производительности.

В данной статье исключительно ради примера используется документо-ориентированная NoSQL база данных MongoDB.

В качестве фреймворка используется Spring WebFlux – реактивная версия Spring MVC.

Построение веб-сервиса

Предположим, что мы разрабатываем веб-сервис для фармацевтики, который позволяет получить и внести данные о лекарствах.

Доменная модель

Напишем класс, описывающий объект «Лекарство»

import lombok.AllArgsConstructor;  
import lombok.Data;  
import lombok.NoArgsConstructor;  
import org.springframework.data.mongodb.core.mapping.Document;


@Document
@Data  
@AllArgsConstructor  
@NoArgsConstructor
public class Drug {

private String internationalId;
private String name;
private String manufacturer;
private Double price;

}

Аннотация @Document — это NoSQL-эквивалент аннотации @Entity. Она сообщает Spring Boot, что этот класс определяет модель данных. В мире NoSQL это означает создание документа вместо записи в таблице.

Документ «Лекарство» имеет четыре свойства: международный идентификатор, название, производитель, цена. Эти свойства автоматически сопоставляются с соответствующими типами BSON для базы данных MongoDB. BSON типы - это типы двоичной сериализации, используемые для сохранения данных в документах MongoDB. Они определяют примитивные типы, которые могут храниться в базе данных MongoDB.

Репозиторий данных

Создав класс Drug с аннотацией @Document, мы сообщили Spring Boot о структуре данных. Далее, для того чтобы эти данные сохранять или загружать из базы данных, необходимо определить репозиторий.

import org.springframework.data.mongodb.repository.ReactiveMongoRepository;  

public interface DrugRepository extends ReactiveMongoRepository<Drug, String> { 
}

ReactiveMongoRepository скрывает реализацию CRUD методов (базовых и не только). Таким образом у нас есть все необходимое, чтобы создавать, обновлять, читать и удалять документы из базы данных MongoDB.

Реализация контроллера с помощю Spring WebFlux

Для того, чтобы веб-клиент имел возможность работать с данными, которые предоставляет наш веб-сервис, необходимо реализовать конечные точки (endpoints).

Для этого создадим класс контроллера.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Controller  
@RequestMapping(path = "/drugs")  
public class DrugController {  
  
  @Autowired
  private DrugRepository dr;


    @PostMapping()
    @ResponseBody 
    public Mono<Drug> addDrugInfo(@RequestBody Drug d) {  
        return dr.save(d);  
    }  
  
    @GetMapping() 
    @ResponseBody   
    public Flux<Drug> findAllDrugs () {  

        Flux<Drug> r = dr.findAll();  
        return r;
    }
}

И так, мы реализовали две конечные точки:

POST /drugs – добавить новое лекарство;

GET /drugs – получить список всех имеющихся в базе данных лекарств.

Главный класс приложения и тестовые данные

Для того, чтобы можно было запустить и протестировать наш веб-сервис, давайте добавим несколько тестовых данных в базу данных.

Для удобства воспользуемся ApplicationRunner (интерфейсом, отмечающим Bean для запуска, как только последний будет загружен в приложение Spring), определив его в главном классе приложения.

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Flux;

@SpringBootApplication
public class DrugServiceApplication {

  public static void main(String[] args) {
    SpringApplication.run(DrugServiceApplication.class, args);
  }

  @Bean
  ApplicationRunner init(DrugRepository dr) {

    Object[][] data = {
        {"IDSN-12345", "Pfizer COVID vaccine", "Pfizer-Italy", 8000.12},
        {"IDSN-6789", "Lopedium", "Sandoz-Germany", 60.00},
        {"IDSN-54321", "Pain Killer", "Neftehim Pharm-Russian Federation", 500.34}
    };

    return args -> {
      dr.deleteAll()
          .thenMany(
              Flux
                  .just(data)
                  .map(array -> {
                    return new Drug(
                             (String) array[0],
                             (String) array[1],
                             (String) array[2],
                             (Double) array[3]);
                  })
                  .flatMap(dr::save)
          )
          .thenMany(dr.findAll())
          .subscribe());
    };
  }
}

 

Использование приложения

Запустите веб-сервис

Попробуйте команду


http GET http://localhost:8080/drugs

Сервис отдаст следующие данные:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
transfer-encoding: chunked
[
    {
        "internationalId": "IDSN-12345",
        "name": "Pfizer COVID vaccine",
        "manufacturer": "Pfizer-Italy",
        "price": 8000.12
    },
    {
        "internationalId ": "IDSN-6789",
        "name": "Lopedium",
        "manufacturer": "Sandoz-Germany",
        "price": 60.00
    },
    {
        "internationalId": "IDSN-54321",
        "name": "Pain Killer",
        "manufacturer": "Neftehim Pharm-Russian Federation",
        "price": 500.34
    }
]

Теперь попробуйте команду


http POST http://localhost:8080/drugs internationalId="IDSN-98765" name="Elixir of Immortality" manufacturer="Beloved mother-in-law" price="777"

Результатом будет добавление в базу нового документа «Лекарство»:

HTTP/1.1 200 OK
Content-Length: ??
Content-Type: application/json;charset=UTF-8
{
    "internationalId": "IDSN-98765",
    "name": "Elixir of Immortality",
    "manufacturer": "Beloved mother-in-law",
    "price": 777.00
}

 

Заключение

В данной статье мы рассмотрели построение простого реактивного веб-сервиса, который обращается к базе данных NoSQL.

Как видно, создание отзывчивых приложений на базовом уровне не вызывает сложностей, к тому же использование фреймворка и готовых подключаемых решений значительно упрощает разработку.