Исходные данные
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.
Как видно, создание отзывчивых приложений на базовом уровне не вызывает сложностей, к тому же использование фреймворка и готовых подключаемых решений значительно упрощает разработку.