Reaktives Beispiel mit Spring 5

Leider gibt es derzeit noch recht wenige Beispiele, die die reaktiven Möglichkeiten von Spring 5 zeigen. Aus diesem Grund habe ich mich entschieden, ein Beispielprojekt zu entwerfen und via Github dem Rest der Welt zur Verfügung zu stellen. Benutzt wird Spring 5 zusammen mit Spring Boot als Basis. Die verwendeten Komponenten sind:

  • Spring Webflux
  • Spring Data 2.0 (der reaktive Teil)
  • Webclient (ein reaktiver Webclient, der in Spring enthalten ist)
  • MongoDB (da asychroner Datenbank-Treiber existiert)

Als Zielsetzung von diesem Projekt soll eine einfache Taskverwaltung implementiert werden.

Initial benötigen wir erstmal die verwendeten Spring-Bibliotheken, sowie andere Abhängigkeiten. Wir verwenden Gradle, um alle Dependencies aufzulösen. Für unseren Zweck funktioniert die Seite start.spring.io ideal, da wir hierüber schnell ein Basisprojekt erstellen können.

Anschließend fügen wir noch evtl. fehlende Dependencies in die build.gradle Datei hinzu. Hierzu gehört z. B. de.flapdoodle.embed:de.flapdoodle.embed.mongo, welches es ermöglicht, eine MongoDB-Instanz über eine Spring Bean zu starten (und wenn notwendig vorher die Binaries herunterzuladen).

compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-webflux')
compile('org.springframework.data:spring-data-mongodb')
compile('io.projectreactor.addons:reactor-extra')

compile('org.mongodb:mongodb-driver-reactivestreams')
compile('de.flapdoodle.embed:de.flapdoodle.embed.mongo')

Nachdem wir in der IDE unserer Wahl nun ein importiertes Basisprojekt haben können wir anfangen den Backend-Service zu definieren.

public interface TaskItemService {
    Flux<TaskItem> getAll();
    Mono<TaskItem> getTask(Long id);
    Mono<TaskItem> createTask(String title, String description, Date dueDate);
    Mono<TaskItem> updateTask(Long id, TaskItem task);
    void finishTask(Long id);
}

Hierbei ist zu beachten, dass wir als Rückgabewert nur reaktive Konstrukte benutzen wollen, weil diese es ermöglichen, dass kein Thread blockiert wird, während der Service auf I/O (die Datenbank) wartet.

Anschließend können wir anfangen, den Service auf unseren Server mit der MongoDB zu verbinden. Hierzu benötigen wir ein Repository, welches von dem Interface ReactiveCrudRepository erbt. Dieses enthält alle aus der klassischen (Spring Data) Welt bekannten Methoden, jedoch sind alle Rückgabewerte reaktive Konstrukte.

@Repository
public interface TaskItemRepository extends ReactiveCrudRepository<TaskItem, Long> {
}

Für unser einfaches Beispiel benötigen wir keine weiteren Methoden, es könnten aber dem Repository weiterhin eigene Query-Methoden hinzugefügt werden.

Anschließend können wir den Service mithilfe des Repositorys implementieren (hier nur einige Methoden exemplarisch aus Platzgründen):

@Service
public class TaskItemServiceImpl implements TaskItemService {

  @Override
  public Flux<TaskItem> getAll() {
      return repository.findAll();
  }
  [...]
  
  @Override
  public Mono<TaskItem> createTask(String title, String description, Date dueDate) {
      final TaskItem task = new TaskItem();
      task.setId(nextId++);
      task.setTitle(title);
      task.setDescription(description);
      task.setDueDate(dueDate);
      return repository.save(task);
  }
  [...]
}

Um unseren Server zu vervollständigen, benötigen wir nun noch den Spring-Webflux-Controller. Hierbei handelt es sich um ein MVC-ähnliches Konstrukt, welches jedoch keine feste Beziehung zwischen Requests und Threads enthält.

Da Spring Security seinen Kontext in einem ThreadLocal speichert, ist dies auch der Grund, warum es nicht auf einem reaktiven Webserver einsetzbar ist.

@RestController
@RequestMapping("task")
public class TaskItemController {

  @Autowired
  private TaskItemService service;
  
  @RequestMapping(method = RequestMethod.GET)
      public Flux<TaskItem> getAll() {
      return service.getAll();
  }
  [...]
  @RequestMapping(value = "{id}", method = RequestMethod.GET)
      public Mono<TaskItem> getTask(@PathVariable("id") Long id) {
      return service.getTask(id);
  }
  [...]
  
  @RequestMapping(value = "{id}", method = RequestMethod.PUT)
   public Mono<TaskItem> updateTask(@PathVariable("id") Long id, @RequestBody TaskItem task) {
     return service.updateTask(id, task);
   }
}

Weil Spring automatisch Objekte in JSON oder alternativ XML umwandeln kann, ist es möglich, mithilfe von @RequestBody sowohl Objekte zu erhalten, als auch in den Rückgabewerten zu benutzen.

Kommen wir nun zum Client. Bei diesem handelt es sich um eine kleine Konsolenanwendung. Diese ist im Wesentlichen relativ einfach und uninteressant. Von Interesse ist jedoch die Kommunikation mit dem Server.

Mithilfe des WebClients von Spring ist es hierbei möglich, vollständig asynchron Requests zu erstellen. Zudem kommt uns auch hier zugute, dass Spring Objekte automatisch nach JSON konvertieren kann.

public class WebClientCommunication {

  // An dieser Stelle weisen wir den Webclient an, einen get()-
  // Request mit JSON als Antwort zu versenden, das Ergebniss 
  // soll dann zu einem TaskItem deserialisiert werden
  public static Mono<TaskItem> getTaskItemFor(Long taskId) {
      return WebClient.create("http://127.0.0.1:8080/task/" + taskId).get().accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(TaskItem.class);
  }
  
  // Mithilfe von einem BodyInserter ist es auch möglich, klassische 
  // Formulardaten zu versenden, und somit auf klassische Webservices zuzugreifen
  public static Mono<ClientResponse> createTaskItem(TaskItem toCreate) {
      final MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
      formData.add("title", toCreate.getTitle());
      formData.add("description", toCreate.getDescription());
      formData.add("dueDate", DateFormatUtils.getDateFormat().format(toCreate.getDueDate()));
      return WebClient.create("http://127.0.0.1:8080/task")
             .post()
             .accept(MediaType.MULTIPART_FORM_DATA)
             .body(BodyInserters.fromMultipartData(formData))
             .exchange();
   }
  
  // Ebenso ist es möglich, über die Inserter einen Body mit der JSON-Repräsentation
  // eines Objektes zu füllen.
  public static Mono<ClientResponse> updateTaskItem(TaskItem toUpdate) {
      return WebClient.create("http://127.0.0.1:8080/task/" + toUpdate.getId())
             .put()
             .accept(MediaType.APPLICATION_JSON)
             .body(BodyInserters.fromObject(toUpdate))
             .exchange();
   }
  [...]
  
  // Die Verwendung von exchange() statt retrieve() sorgt dafür, dass wir uns nicht um
  // die Antwort kümmern müssen. Es wird stattdessen ein ClientResponse zurückgegeben, 
  // und mithilfe des Monos können Fehler signalisiert werden (Http Code-basiert).
  public static Mono<ClientResponse> setDone(Long id) {
      return WebClient.create("http://127.0.0.1:8080/task/" + id + "/finish")
             .post()
             .accept(MediaType.APPLICATION_JSON)
             .exchange();
  }
} 

An dieser Stelle sind wir bereits fertig mit unserer komplett reaktiven Umsetzung.

  1. Wenn der Client einen Request stellt
  2. wird hierfür via Webclient auf NIO-Basis asynchron eine Verbindung aufgebaut.
  3. Diese wird dann von Webflux ebenfalls asynchron an unser Backend weitergegeben,
  4. welches wiederum asynchron mit der MongoDB kommuniziert.

Durch die reaktive Umsetzung kann diese Implementierung mit erheblich weniger Ressourcen deutlich mehr Requests verarbeiten, da die Kosten für Thread- und Kontextwechsel minimiert wurden. Gerade bei einem Webserver können diese erheblich höher sein, als die eigentliche Last, die durch die Verarbeitung der Daten verursacht wird.

Ein Vergleich von WebMVC und Webflux gibt es z. B. hier.

Der komplette lauffähige Code von dem obigen Beispiel kann hier heruntergeladen werden:

https://github.com/KaiBoernert/Spring5-Reactor-Reactive-Example

Short URL for this post: https://wp.me/p4nxik-2Vo
This entry was posted in Spring Universe and tagged , , . Bookmark the permalink.

Leave a Reply