Implementare il pattern Post/Redirect/Get con Spring MVC

Abbiamo già affrontato il tema delle form submission tempo fa, ma torniamo sull’argomento in questo post per parlare di un problema tipico dell applicazioni web basate sul pattern MVC.

Il problema è quello delle submission multiple e, purtroppo, anche la nostra cara SuperRubricaTelefonica! ne è affetta…

Fortunatamente una soluzione c’è e consiste nell’applicare il pattern PRG (Post/Redirect/Get). In questo articolo vediamo come implementarlo nella nostra web application, per poi fare la conoscenza dei RedirectAttributes di Spring MVC.

Il problema delle submission multiple

Nella puntata precedente abbiamo parlato di salvataggio dei dati. Abbiamo visto come configurare un database per la nostra rubrica e come implementare le operazioni di scrittura e lettura dei contatti.

Forse però non vi siete accorti di un piccolo(?) dettaglio…

Facciamo un test: lanciate la SuperRubricaTelefonica e inserite un nuovo contatto tramite l’apposito form. Una volta effettuato il salvataggio dovreste ritrovarvi nella pagina di dettaglio, con il riepilogo dei dati.

Ora premete ripetutamente il tasto F5Cosa succede?

Apparentemente nulla di particolare: state semplicemente refreshando la pagina di riepilogo. Niente di più innocuo…

E invece no!

Se state usando un browser sufficientemente “intelligente”, forse avrete già capito che non si tratta di un semplice refresh:

Refresh?

E in effetti se date un’occhiata alla lista contatti, dovreste notare qualcosa di strano: secondo i miei calcoli, avrete un bel po’ di contatti duplicati.

Se analizziamo i risultati del nostro piccolo esperimento, ci rendiamo conto che ogni volta che abbiamo premuto il tasto F5 non stavamo semplicemente aggiornando la pagina di riepilogo (ovvero non stavamo effettuando una richiesta GET al server), in realtà stavamo sottomettendo nuovamente il form (ovvero una nuova POST ad ogni pressione del tasto).

Brutta storia, vero?

Il pattern PRG

Come già accennato, possiamo risolvere il problema appena esaminato applicando il pattern PRG ai nostri Controller.

Ma in cosa consiste il pattern Post/Redirect/Get?

Il concetto è semplice: invece di mostrare la pagina di avvenuta submission come risultato della POST, si effettua una redirect alla stessa pagina.

La differenza è sottile, ma sostanziale: nel momento in cui ci venisse in mente di effettuare il refresh della pagina, non ci sarebbe il rischio di sottomettere nuovamente i dati precedentemente inviati poiché, in questo caso, stiamo effettivamente inviando una richiesta GET al server.

A livello pratico, il codice del nostro ContactController, e in particolare quello del metodo submitNewContact, cambia in questo modo:

@PostMapping("/new")
public String submitNewContact(@Valid @ModelAttribute ContactForm contactForm
  , BindingResult bindingResult) {

  if (bindingResult.hasErrors()) {
    return "contact-form";
  }

  ContactDetails cnt = contactService.save(contactForm);
  return "redirect:/contacts/" + cnt.getId();
}

Mentre prima restituivamo il nome della View da visualizzare (con annesso attributo inserito nel Model), ora rimandiamo direttamente alla chiamata relativa al dettaglio contatto.

Il prefisso redirect: di Spring MVC ci consente di dichiarare, in modo molto semplice, la redirect nella return statement del nostro metodo.

I redirect attributes

Come abbiamo visto il pattern Post/Redirect/Get risolve elegantemente il problema delle submission multiple; allo stesso tempo però crea un piccolo ostacolo.

Se ricordate bene, nella vecchia versione del metodo submitNewContact, aggiungevamo al Model il parametro contact, utilizzato nella View di riepilogo. Nella nuova versione non abbiamo bisogno di passare il nuovo contatto appena creato, però potremmo aver bisogno di passare qualche altro tipo d’informazione. Ad esempio, un flag per visualizzare un messaggio di conferma.

Ovviamente, in presenza di una redirect, il vecchio approccio risulta inapplicabile, in quanto il contenuto del Model “sopravvive” solamente nel periodo di vita della richiesta corrente.

Per fortuna, il nostro caro Spring ci viene in aiuto con un formidabile strumento: l’interfaccia RedirectAttributes.

Come Model, anche RedirectAttributes rappresenta un contenitore di elementi che fa da ponte tra Controller e View. La particolarità di RedirectAttributes sta nel fatto di essere specificamente utilizzata per il passaggio di attributi attraverso un evento di redirect.

L’interfaccia prevede due tipi di attributo: quelli che possiamo definire classici ed i flash attributes. La differenza tra i due sta nelle modalità di passaggio degli oggetti alla View.

Mentre gli attributi classici vengono inviati nel corpo della richiesta (nella fattispecie in query-string), i flash attributes vengono mantenuti in sessione giusto il tempo di essere recuperati nella View, dopodiché vengono rimossi.

Aggiorniamo il Controller

Vediamo dunque come applicare i flash attributes nel nostro Controller:

@PostMapping("/new")
public String submitNewContact(@Valid @ModelAttribute ContactForm contactForm
  , BindingResult bindingResult
  , RedirectAttributes attributes) {

  if (bindingResult.hasErrors()) {
    return "contact-form";
  }

  ContactDetails cnt = contactService.save(contactForm);
  attributes.addFlashAttribute("newContact", true);
  return "redirect:/contacts/" + cnt.getId();
}

Come vedete, ho aggiunto RedirectAttributes ai parametri del metodo submitNewContact. Inoltre ho creato un nuovo flash attribute che servirà a segnalare la creazione di un nuovo contatto.

Nel metodo che gestisce il dettaglio del contatto, invece, ho aggiunto qualche linea di codice per recuperare l’attributo newContact:

@GetMapping("/{id}")
public String contactById(@PathVariable("id") Long id, Model model) {
  ContactDetails contact = contactService.getDetailsById(id);

  if (contact == null) {
    return "redirect:/";
  }

  Object newContactFlag = model.asMap().get("newContact");
  if(newContactFlag != null && (boolean) newContactFlag){
    model.addAttribute("newContactFlag", true);
  }
  model.addAttribute("contact", contact);
  return "contact-details";
}

Una volta recuperato, il flash attribute newContact va “convertito” in attributo classico aggiungendolo nuovamente al Model, in questo modo sarà disponibile nel contesto dell’attuale richiesta.

L’attributo newContactFlag verrà utilizzato nella pagina di dettaglio per visualizzare un alert nel caso in cui il contatto sia stato appena creato.

Alert di conferma

Per mostrare il box di conferma, come al solito, ci facciamo aiutare da Thymeleaf e Bootstrap:

<div th:if="${newContactFlag != null}"
  class="alert alert-success alert-dismissable">
  <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
  Contatto creato correttamente
</div>

In conclusione

L’invio multiplo dei form è un problema che passa a volte in secondo piano, ma che può avere effetti inaspettati, anche gravi.

Meglio dunque gestire fin da subito questo aspetto, tramite l’implementazione del pattern PRG nei nostri Controller.

Nel prossimo appuntamento con la nostra SuperRubricaTelefonica! parliamo di autenticazione con Spring Security.

Rimanete sintonizzati! ;-)

Ciao,

David