Tworzenie aplikacji reaktywnych w Spring Boot
Programowanie reaktywne jest jednym z paradygmatów programowania. O tym jakie cechy wyróżniają system reaktywny, mówi tzw. manifest reaktywny i jego podstawowe cztery postulaty.
Te postulaty to:
- responsywność — system musi reagować na interakcje użytkowników,
- niezawodność — system musi reagować na awarie i należeć do grupy systemów wysokiej dostępności
- elastyczność — system musi reagować na zmieniające się obciążenie,
- sterowanie za pomocą komunikatów — system musi dynamicznie reagować na wprowadzane dane.
Cezary Rejczyk. Senior Java Developer w Capsilon. Od prawie 11 lat pracuje jako programista, głównie w sektorze bankowym. Jest zwolennikiem programowania mistrzowskiego i ekstremalnego. Pasjonat gór, skitouring i biegów górskich.
Sama idea oczywiście nie jest nowa, a patrząc na wymienione cechy niekoniecznie musimy iść w kierunku programowania reaktywnego, aby osiągnąć zamierzone postulaty. Jednak programowanie reaktywne coraz mocniej puka do drzwi programistów, warto zatem zapoznać się z podstawowymi bibliotekami, które ułatwią nam wejście w ten świat. W artykule chciałbym przybliżyć temat tworzenia aplikacji reaktywnych w oparciu w Spring Boot.
Spring MVC vs Spring WebFlux
Spring MVC to webowe rozszerzenie obecnie najpopularniejszego frameworka do tworzenia aplikacji w języku Java – Spring-a. Jest to już wyżarzony framework, stanowiący podstawę działania wielu aplikacji na platformę JVM. Przykład takiej aplikacji został stworzony także na potrzeby publikacji. W sieci znajduje się wiele publikacji i kursów, jak szybko rozpocząć przygodę z tworzeniem aplikacji webowych w Spring MVC. Nie będziemy się tutaj wdawać w szczegóły, gdyż to nie jest temat tego artykułu. Celem aplikacji było stworzenie restowego API oraz pokazanie event-stream, z wykorzystaniem Spring-owego – Server-Sent Events. Zachęcamy do pobrania aplikacji i zapoznania się kodem.
Czy zatem warto zainteresować się Spring WebFlux, który wspiera programowanie reaktywne? Spring WebFlux wykorzystuje bibliotekę o nazwie Reactor do obsługi reaktywnej. Oczywiście, jak już zostało wcześniej wspomniane, ciągły rozwój i poznawanie nowych technologii, to chleb powszedni dla każdego dewelopera. Jednak w tym wypadku chciałbym zwrócić uwagę nad przewagą podejścia reaktywnego w stosunku do “klasycznego”. Model MVC wprowadza granicę pomiędzy wątkami kontenera (1 żądanie = 1 wątek) i przetwarzanie żądania w aplikacji.
W tym modelu potrzebujemy wielu wątków, aby osiągnąć współbieżność. Kod nieblokujący (reaktywny) wymaga tylko kilku wątków do jednoczesnego przetwarzania wielu żądań. Model ten został wykorzystany w bardzo popularnym obecnie Node.js, a WebFlux dostarcza nam stosu technologicznego dla platformy JVM.
Z pewnością przejście na stos reaktywny wiąże się ze zmianą sposobu myślenia o naszej aplikacji. Na dzień dzisiejszy technologia posiada także spore ograniczenia. Chyba największy problem stanowi brak reaktywnych konektorów dla SQL-owych baz danych. Oczywiście na githubie możemy odnaleźć kilka projektów związanych z tym tematem, jednak nie są to biblioteki oficjalnie wspierane przez PostgreSQL lub MySQL. Kiedy zatem warto zastanowić się nad wykorzystaniem reaktywnego klienta, nawet jeśli nie możemy pozwolić sobie na przejście na bazę NoSQL. Z pewnością takim miejscem w naszej aplikacji bazującej na frameworku Spring są wszelkiego rodzaju wywołania serwisów zewnętrznych lub mikrousług wewnętrznych. Klasyczne MVC blokuje wątek na czas wywołania usługi, klient reaktywny zwraca wątek na czas wywołania usługi do puli wątków, w momencie otrzymania odpowiedzi dane są przetwarzane przez nowy wątek pobrany z puli.
Na koniec tej części publikacji chciałbym rzucić trochę światła na kwestię wydajności. Oczywiście, testy zostały wykonane w warunkach laboratoryjnych, a nie na systemie produkcyjnych. Łatwo w nich zauważyć bolączkę systemów opartych o servlety.
SpringBoot 2 Servlet vs Reactive performance — 10000 users (4 requests/user)
Przy stosunkowo małym obciążeniu nie widać przewagi stosu reaktywnego. Jednak sytuacja zaczyna się zmieniać, wraz ze wzrostem obciążenia. Dla dużego obciążenia średnie czasy odpowiedzi aplikacji bazującej na serwletach gwałtownie rosną, przyczyną jest oczywiście problem poruszany we wcześniejszej części. Domyślna pula wątków dla kontenera aplikacji np. Tomcat wynosi 200, szybko ulega ona wyczerpaniu, co powoduje wzrost czasu oczekiwania na obsługę kolejnych żądań.
Źródło: https://github.com/raj-saxena/spring-boot-1-vs-2-performance
Przykład aplikacji reaktywnej z użyciem Spring Boot 2 i MongoDB
Stos reaktywny w Spring umożliwia nam szybkie rozpoczęcie przygody z programowaniem reaktywnych aplikacji internetowych. Oczywiście na rynku jest szereg implementacji dla programowania reaktywnego(RxJava), z których także możemy skorzystać. Na potrzeby publikacji został stworzony prosty projekt, który przybliży czytelnikom wejście w świat reaktywnych aplikacji.
Na początek chciałbym przybliżyć dwa podstawowe typy, czyli Mono i Flux. Są to strumienie, które reprezentują sekwencje reaktywne. Flux może emitować zero lub więcej pozycji (strumienie wielu elementów), Mono może emitować zero lub tylko jeden element.
Istnieją dwie kategorie sekwencji reaktywnych. Cold, wydawcy zaczynają generować dane tylko wtedy, gdy otrzymają nową subskrypcję. Jeśli nie ma subskrypcji, dane nigdy nie docierają do strumienia. Hot, wydawcy nie potrzebują żadnych subskrybentów do generowania przepływu danych. Kiedy nowy subskrybent jest zarejestrowany, otrzyma tylko nowe elementy danych.
Zdaję sobie sprawę, że są to byty abstrakcyjne, myślę że konkretny przykład zastosowania rozjaśni nieco spojrzenie. W celu zapoznania się z API stosu reaktywnego, zachęcamy do odwiedzenia strony z dokumentacją.
1. Wchodzimy na stronę stronę i tworzymy strukturę projektu
2. Wygenerowany projekt importujemy do naszego ulubionego IDE
3. Tworzymy prosty model danych
package com.softmill.reactivespringboot.reactivespringboot.model; import java.util.Date; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @Document(collection = "tweets") public class Tweet { @Id private String id; @NotBlank @Size(max = 140) private String text; @NotNull private Date createdAt = new Date(); public Tweet() { } public Tweet(String text) { this.text = text; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Date getCreatedAt() { return createdAt; } public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } }
4. Tworzymy repozytorium
package com.softmill.reactivespringboot.reactivespringboot.repository; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.stereotype.Repository; import com.softmill.reactivespringboot.reactivespringboot.model.Tweet; @Repository public interface TweetRepository extends ReactiveCrudRepository<Tweet, String> { }
5. Tworzymy punkty dostępowe REST
package com.softmill.reactivespringboot.reactivespringboot.controller; import java.time.Duration; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.softmill.reactivespringboot.reactivespringboot.model.Tweet; import com.softmill.reactivespringboot.reactivespringboot.repository.TweetRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController public class TweetController { @Autowired private TweetRepository tweetRepository; @GetMapping("/tweets") public Flux<Tweet> getAllTweets() { return tweetRepository.findAll(); } @PostMapping("/tweets") public Mono<Tweet> createTweets(@Valid @RequestBody Tweet tweet) { return tweetRepository.save(tweet); } @GetMapping("/tweets/{id}") public Mono<ResponseEntity<Tweet>> getTweetById(@PathVariable(value = "id") String tweetId) { return tweetRepository.findById(tweetId).map(savedTweet -> ResponseEntity.ok(savedTweet)) .defaultIfEmpty(ResponseEntity.notFound().build()); } @PutMapping("/tweets/{id}") public Mono<ResponseEntity<Tweet>> updateTweet(@PathVariable(value = "id") String tweetId, @Valid @RequestBody Tweet tweet) { return tweetRepository.findById(tweetId).flatMap(existingTweet -> { existingTweet.setText(tweet.getText()); return tweetRepository.save(existingTweet); }).map(updatedTweet -> new ResponseEntity<>(updatedTweet, HttpStatus.OK)) .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } @DeleteMapping("/tweets/{id}") public Mono<ResponseEntity<Void>> deleteTweet(@PathVariable(value = "id") String tweetId) { return tweetRepository.findById(tweetId) .flatMap(existingTweet -> tweetRepository.delete(existingTweet) .then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))) .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } @GetMapping(value = "/stream/tweets", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Tweet> streamAllTweets() { return tweetRepository.findAll().delayElements(Duration.ofMillis(100)); } }
Projekt demonstracyjny dostępny jest pod linkiem. Zachęcam do pobierania z repozytorium i modyfikacji. W celu uruchomienia aplikacji wykonujemy polecenie:
$ mvn spring-boot:run
Domyślnie aplikacja zostanie uruchomiona lokalnie na porcie 9000. W przykładowym projekcie, podobnie jak w MVC, została stworzona wizualizacja monitorująca zapisane dane. Tutaj warto zwrócić uwagę, że nasz pulpit z danymi odświeża się automatycznie, dzięki wykorzystaniu Reactive Streams. Implementacja tej funkcjonalności jest znacznie prostsza, niż miało to miejsce w oparciu o Server-Sent Events.
Mam nadzieję, że udało się zainteresować Was programowaniem reaktywnym i jego praktycznym zastosowaniem. Oczywiście temat jest bardzo szeroki, a nauczenie się operacji na strumieniach wymaga czasu i praktyki. Trudno jest oprzeć się wrażeniu, że jest to naturalny kierunek rozwoju tworzenia nowoczesnych aplikacji webowych.