Block Image

In questo articolo vedremo il nuovo stack Reactive di Spring 5 per creare servizi REST, Spring WebFlux.

Cos'è un sistema reattivo

Da Wikipedia:

«La programmazione reattiva è un paradigma di programmazione orientata sui flussi di dati e la propagazione del cambiamento.»

Il Manifesto Reactive descrive le caratteristiche che un sistema reattivo deve avere:

  • Responsive. Il sistema deve fornire risposte veloci ed essere affidabile.
  • Resiliente. Il sistema deve essere in grado di contenere eventuali failures attraverso l'isolamento. Se qualcosa va storto, il sistema deve reagire senza bloccarsi.
  • Elastico. Il sistema deve supportare algoritmi predittivi per evitare colli di bottiglia, incrementando e decrementando risorse a seconda di quanti dati arrivano in input.
  • Message driven. Il sistema deve basarsi sul paradigma message-driven, poichè i componenti devono poter comunicare tra loro tramite messaggi asincroni.

In Java esiste la specifica Reactive Streams che definisce standard per sistemi reattivi.
Ci sono poi varie implementazioni di questa specifica, come RxJava, Project Reactor, Vert.X.

Le interfacce fornite da questa specifica sono:

public interface Publisher<T> {
    void subscribe(Subscriber<? super T> var1);
}

public interface Subscriber<T> {
    void onSubscribe(Subscription var1);

    void onNext(T var1);

    void onError(Throwable var1);

    void onComplete();
}

public interface Subscription {
    void request(long var1);

    void cancel();
}

public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

Dalle interfacce si evince che viene applicato il pattern Observer.
In particolare:

  • Il Publisher pubblica elementi e fornisce il metodo subscribe per permettere ai Subscriber di rimanere in ascolto.
  • Il Subscriber si sottoscrive al Publisher e riceve i dati tramite il metodo onNext.
  • Un Subscription rappresenta il collegamento tra il Publisher e il Subscriber. Tramite il metodo cancel, il subscriber può cancellare la sottoscrizione e tramite il metodo request pùo implementare il meccanisco di backpressure.
  • Un Processor è sia un Publisher che un Subscriber, quindi può sia rimanere in ascolto e sia pubblicare elementi.

Spring WebFlux usa la libreria Project Reactor ed utilizza di default Netty come server reattivo non bloccante.

Project Reactor: Mono e Flux

Project Reactor è il framework di programmazione reattiva non bloccante utilizzato da Spring.
Esso fornisce due API che implementano l'interfaccia Publisher della specifica Reactive Streams:

  • Mono: per gestire 0 o 1 elemento asincrono.
  • Flux: per gestire 0 o N elementi asincroni.

Per ulteriori informazioni su Project Reactor, su come funziona la programmazione reattiva e i vantaggi rispetto all'uso di Callbacks e Futures, potete visitare il sito
https://projectreactor.io/docs/core/release/reference/.

Spring WebFlux permette di creare servizi REST in due modalità:

  • modalità funzionale (tramite RouterFunction e HandlerFunction)
  • modalità tradizionale (annotation based, usando annotazioni come @RestController, @RequestMapping)

Creiamo ora un semplice Web Service REST con Spring WebFlux utilizzando Reactive Mongo!

Prerequisiti

  1. Aver installato una JDK (useremo la versione 17 ma va bene anche una successiva alla 7).

Primo passo: andare sul sito Spring Initializr

Questo sito creerà per noi uno scheletro di un'app Spring Boot con tutto quello che ci serve (basta cercare le dipendenze che ci servono nella sezione 'Dependencies'). Clicchiamo su 'ADD DEPENDENCIES' ed aggiungiamo le dipendenze riportate dall'immagine.

Block Image

  • La dipendenza di Spring Reactive Web serve per utilizzare WebFlux.
  • Spring Data Reactive MongoDB serve per utilizzare il repository reattivo per MongoDB.
  • Validation serve per utilizzare la validazione tramite javax annotations usando Hibernate Validator.
  • Lombok ci permette con delle annotations di auto-generare i metodo getters, setters, equals, hashcode e toString.
  • Embedded MongoDB Database ci permette di utilizzare un MongoDB embedded (un po' come facciamo con H2).

Cliccate su 'Generate'. Verrà scaricato lo zip del progetto. Sostituiamo lo scope "test" con "runtime" per la libreria de.flapdoodle.embed.mongo in quanto useremo MongoDb embedded per sviluppo e non solo per test.
Aggiungiamo infine, nel file application.properties, la versione di MongoDb che vogliamo utilizzare:
spring.mongodb.embedded.version=5.0.0

Secondo passo: creiamo una classe di dominio User che mapperà i medesimi Documents in MongoDB

@Document
@Data
@NoArgsConstructor
public class User {

    @Id
    private String id;

    private String name;

    @NotBlank
    private String surname;

    public User(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }
}

L'annotation @Document marca la classe come oggetto persistente di MongoDB.
@Data e @NoArgsConstructor sono annotations di Lombok.


Terzo passo: creiamo un Repository Reactive per User

public interface UserMongoRepository extends ReactiveMongoRepository<User, String> {
}

Come tutti i repositories Spring, questo approccio fornisce metodi come findAll, findById, save, delete out of the box per la classe di dominio User.


Quarto passo: scriviamo una classe handler generale per gestire la validazione

@Component
@RequiredArgsConstructor
public class ValidatorHandler {

    private final Validator validator;

    public <T> void validate(T o) {
        Set<ConstraintViolation<T>> validate = validator.validate(o);
        if(! validate.isEmpty()) {
            ConstraintViolation<T> violation = validate.stream().iterator().next();
            throw new ServerWebInputException(violation.toString());
        }
    }
}

Se la validazione rileva almeno una violazione, viene lanciata l'eccezione ServerWebInputException che estende ResponseStatusException e restituisce quindi anche 400 badRequest come HTTP Status.

Quinto passo: scriviamo l'handler per gli endpoints che gestiscono User

@Component
@RequiredArgsConstructor
public class UserHandler {


    private static final Logger LOGGER = LoggerFactory.getLogger(UserHandler.class);

    private final UserMongoRepository userMongoRepository;

    private final ValidatorHandler validatorHandler;


    public Mono<ServerResponse> findAll(ServerRequest request) {
        return userMongoRepository.findAll()
                .collectList()
                .flatMap(users -> {
                    if (users.isEmpty()) {
                        return ServerResponse.noContent().build();
                    }
                    return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(fromValue(users));
                });
    }

    public Mono<ServerResponse> save(ServerRequest request) {
        return request.bodyToMono(User.class)
                .doOnNext(validatorHandler::validate)
                .flatMap(userMongoRepository::save)
                .doOnSuccess(userSaved -> LOGGER.info("User saved with id: " + userSaved.getId()))
                .doOnError(e -> LOGGER.error("Error in saveUser method", e))
                .map(userSaved -> UriComponentsBuilder.fromPath(("/{id}")).buildAndExpand(userSaved.getId()).toUri())
                .flatMap(uri -> ServerResponse.created(uri).build());
    }
}

Analizziamo il findAll:

  1. Richiamiamo il findAll del repository che restituisce un Flux.
  2. Con il metodo collectList() trasformiamo il Flux in Mono<List>.
  3. Con il metodo flatMap(), trasformiamo infine l'oggetto Mono in ServerResponse (l'equivalente di ResponseEntity per Spring WebFlux), che restituisce 200 OK o 204 NoContent a seconda se la lista sia non vuota o vuota.

Analizziamo il metodo save:

  1. Con request.bodyToMono(User.class) estrapoliamo il body mandato dalla request e lo mappiamo con la classe User.
  2. Con doOnNext effettuiamo al passo successivo, la validazione dell'oggetto User.
  3. Con flatMap trasformiamo l'oggetto User appena creato e validato, in oggetto User creato dal repository, che rappresenta l'oggetto salvato nel DB e che avrà quindi l'id valorizzato.
  4. Con map trasformiamo l'oggetto appena salvato in un oggetto URI che conterrà l'id dello User, così da inserirlo nel campo Location dell'Header Response.
  5. Con flatMap trasformiamo l'URI in oggetto ServerResponse, che restituisce 201 Created con il campo Location valorizzato.
  6. Con doOnSuccess, che viene richiamato se alla fine del flusso non ci sono stati errori, logghiamo l'id dello User salvato.
  7. Con doOnError, che viene richiamato nel caso in cui il flusso sia andato in errore, logghiamo l'eventuale eccezione.

Alla fine dell'articolo trovate il link GitHub del progetto completo anche dei metodi findById e delete.

Sesto passo: scriviamo gli endopints in modo funzionale

Prima di scrivere gli endpoints, diamo uno sguardo all'interfaccia RounterFunction:

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
    Mono<HandlerFunction<T>> route(ServerRequest var1);

    default RouterFunction<T> and(RouterFunction<T> other) {
        return new SameComposedRouterFunction(this, other);
    }
    // other default methods
}

Come possiamo notare, è un'interfaccia funzionale (un'interfaccia con un solo metodo astratto). Noi dovremo quindi implementare il metodo route. ServerRequest invece rappresenta la richiesta HTTP che viene fatta da un client.

Possiamo utilizzare la classe RouterFunctions di Spring che ha il metodo statico route:

public abstract class RouterFunctions {

    public static <T extends ServerResponse> RouterFunction<T> route(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
        return new RouterFunctions.DefaultRouterFunction(predicate, handlerFunction);
    }
//other methods
}

RequestPredicate e HandlerFunction sono a loro volta altre due interfacce funzionali:

@FunctionalInterface
public interface RequestPredicate {
    boolean test(ServerRequest var1);
 //other default methods
}

@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
    Mono<T> handle(ServerRequest var1);
}

Vediamo ora come realizzare gli endpoints con queste interfacce:

@Configuration
public class UserRouter {
    
    @Bean
    public RouterFunction<ServerResponse> findAllRouter(UserHandler userHandler) {
        return route(GET("/users")
                .and(accept(MediaType.APPLICATION_JSON)), userHandler::findAll);
    }

    @Bean
    public RouterFunction<ServerResponse> save(UserHandler userHandler) {
        return route(POST("/users")
                .or(PUT("/users"))
                .and(accept(MediaType.APPLICATION_JSON)), userHandler::save);
    }
    
}

Come possiamo vedere, è una banale classe annotata con @Configuration.
Il primo metodo gestisce l'endpoint /user con verbo GET, mentre il secondo l'endpoint /user con verbi POST e PUT.

Sesto passo B: vediamo gli stessi endpoints scritti nel modo tradizionale

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserResource {

    private final UserMongoRepository userMongoRepository;

    @GetMapping
    public Mono<ResponseEntity<List<User>>> findAll() {
        return userMongoRepository.findAll()
                .collectList()
                .map(users -> {
                    if(users.isEmpty()) {
                        return ResponseEntity.noContent().build();
                    }
                    return ResponseEntity.ok(users);
                });
    }

    @RequestMapping(method = {RequestMethod.POST, RequestMethod.PUT})
    public Mono<ResponseEntity<User>> save(@Valid @RequestBody User user) {
        return userMongoRepository.save(user)
                .map(userSaved -> UriComponentsBuilder.fromPath(("/{id}")).buildAndExpand(userSaved.getId()).toUri())
                .map(uri -> ResponseEntity.created(uri).build());
    }

}

Da notare che le implementazioni dei metodi findAll e save, assomigliano molto a quelle della classe UserHandler.

Il vantaggio che balza all'occhio è che nel metodo tradizionale, la validazione viene fatta in modo automatico con l'annotation @Valid (così come quando usiamo Spring MVC).

Potremmo anche centralizzare la gestione della validazione, e in generale, la gestione delle eccezioni, creando un ExceptionHandler come questo:

@RestControllerAdvice
@Slf4j
public class ExceptionHandlerConfig {

    //handle the Exceptions as you want

    @ExceptionHandler(WebExchangeBindException.class)
    public void handleException(WebExchangeBindException e) {
        log.error("Error Validation", e);
        throw e;
    }
}

Settimo passo: creiamo la classe di configurazione per utilizzare MongoDB embedded

@Configuration
@Profile("dev")
@RequiredArgsConstructor
public class MongoConfig extends AbstractReactiveMongoConfiguration {

   private final Environment env;

   @Override
   protected String getDatabaseName() {
      return "users";
   }

   @Override
   @Bean
   @DependsOn("embeddedMongoServer")
   public MongoClient reactiveMongoClient() {
      var port = env.getProperty("local.mongo.port", Integer.class);
      return MongoClients.create(String.format("mongodb://localhost:%d", port));
   }

   @Bean
   public CommandLineRunner insertData(UserMongoRepository userMongoRepository) {
      return args -> {
         userMongoRepository.save(new User("Vincenzo", "Racca")).subscribe();
         userMongoRepository.save(new User("Mario", "Rossi")).subscribe();
         userMongoRepository.save(new User("Gennaro", "Esposito")).subscribe();
         userMongoRepository.save(new User("Diego", "della Lega")).subscribe();
      };
   }
}

Inseriamo anche cinque records nel db allo startup.

Testiamo gli endpoints con un client REST come Postman

Proviamo gli endppoint esegundo la classe main di Spring e facciamo una chiamata GET a
localhost:8080/users.
Avremo una response simile a questa:

[
    {
        "id": "60340ab46d597e20886009b8",
        "name": "Mario",
        "surname": "Rossi"
    },
    {
        "id": "60340ab46d597e20886009b7",
        "name": "Vincenzo",
        "surname": "Racca"
    },
    {
        "id": "60340ab46d597e20886009b9",
        "name": "Gennaro",
        "surname": "Esposito"
    },
    {
        "id": "60340ab46d597e20886009ba",
        "name": "Diego",
        "surname": "della Lega"
    }
]

Proviamo ora a inserire un nuovo User, invocando una POST a localhost:8080/users con request body:

{
    "name": "Beppe",
    "surname": ""
}

Avremo come HTTP response status 400 BadRequest e il seguente json di risposta:

{
    "timestamp": "2021-02-22T19:54:10.188+00:00",
    "path": "/users",
    "status": 400,
    "error": "Bad Request",
    "message": "ConstraintViolationImpl{interpolatedMessage='non deve essere spazio', propertyPath=surname, rootBeanClass=class com.vincenzoracca.webflux.domains.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}",
    "requestId": "9c502952-5"
}

Lato applicativo avremo questa loggatura:

ERROR 5280 --- [ctor-http-nio-4] c.v.webflux.handlers.UserHandler         : Error in saveUser method

org.springframework.web.server.ServerWebInputException: 400 BAD_REQUEST "ConstraintViolationImpl{interpolatedMessage='non deve essere spazio', propertyPath=surname, rootBeanClass=class com.vincenzoracca.webflux.domains.User, messageTemplate='{javax.validation.constraints.NotBlank.message}'}"
	at com.vincenzoracca.webflux.handlers.ValidatorHandler.validate(ValidatorHandler.java:21) ~[classes/:na]

Questa loggatura è data dalla riga:

.doOnError(e -> LOGGER.error("Error in saveUser method", e))

del metodo save della classe UserHandler. Inseriamo ora una request valida:

{
    "name": "Beppe",
    "surname": "De Rossi"
}

Avremo come HTTP Response Status 201 Created, con response body vuota e nell'Header di risposta avremo il campo Location valorizzato con l'id dello User appena creato.

Testiamo con WebTestClient

Spring WebFlux offre anche un Client REST reattivo non bloccante: WebClient.
Come per la controparte non reattiva, RestTemplate, anche questo client ha una classe che facilita la fase di test, WebTestClient. Vediamo come usarla:

@SpringBootTest
class SpringWebFluxApplicationTests {

    @Autowired
    private UserRouter userRouter;

    @Autowired
    private UserHandler userHandler;

    @MockBean
    private UserMongoRepository userMongoRepository;

    @Test
    public void findAllTest() {

        WebTestClient client = WebTestClient
                .bindToRouterFunction(userRouter.findAllRouter(userHandler))
                .build();

        List<User> users = Arrays.asList(new User("Mario","Rossi"),
                new User("Filippo", "Bianchi"));

        Flux<User> flux = Flux.fromIterable(users);
        given(userMongoRepository.findAll())
                .willReturn(flux);

        client.get().uri("/users")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(User.class)
                .isEqualTo(users);
    }
    
    @Test
    public void saveTest() {

        WebTestClient client = WebTestClient
                .bindToRouterFunction(userRouter.save(userHandler))
                .build();

        User user = new User("Clark","Kent");
        user.setId("efgt-fght");

        Mono<User> mono = Mono.just(user);
        given(userMongoRepository.save(user))
                .willReturn(mono);

        client.post().uri("/users")
                .accept(MediaType.APPLICATION_JSON)
                .body(mono, User.class)
                .exchange()
                .expectStatus().isCreated()
                .expectHeader()
                .location("/efgt-fght");
    }
}

Due cose da notare:

  • UserMongoRepository non viene iniettato dal contesto di Spring, ma viene mockato. In questa fase non ci serve andare su un database (che sia un DB reale o di test).

  • La classe WebTestClient viene creata a partire da un RouterFunction, per questo iniettiamo i reali UserRouter e UserHandler.

Conclusioni

Abbiamo visto come creare un semplice Web Service REST con il nuovo modulo Reactive Spring WebFlux e Reactive MongoDB.
Su internet è possibile trovare articoli molto interessanti sulle performance di WebFlux rispetto all'uso delle tradizionali Servlet bloccanti come Tomcat.

Potete trovare il progetto completo sul mio github a questo link: Spring WebFlux

Articoli su Spring: Spring

Libri consigliati su Spring: