Mapping delle richieste HTTP in Spring MVC

In questo post approfondiamo il tema della gestione delle richieste HTTP in un’applicazione Spring MVC.

Riprendiamo la web application creata nel precedente articolo ed esaminiamo più da vicino l’utilizzo dell’annotazione @RequestMapping nei nostri Controller.

Prima, però, occorre fare una breve premessa…

La DispatcherServlet, in breve…

Il cuore di Spring MVC è costituito dalla DispatcherServlet, l’elemento che implementa i meccanismi di routing delle richieste HTTP.

In sintesi, il framework si basa su una singola servlet che ha il compito di intercettare le richieste dirette verso l’applicazione, per poi “smistarle” ai Controller di competenza.

Se stiamo lavorando con Spring Boot, il nostro progetto disporrà già di una configurazione di default. In particolare, la nostra applicazione disporrà di una dispatcher servlet pronta a gestire tutte le richieste, ovvero tutte quelle con url corrispondenti al pattern /*.

Nota In realtà Spring MVC dà la possibilità di definire nel nostro progetto più di una DispatcherServlet (ciascuna con un differente mapping e un application context di riferimento), così da poter gestire differenti gruppi di richieste in modo specifico.

Come già accennato, quindi, sono i Controller a farsi carico della gestione vera e propria delle request, le quali vengono “recapitate” in base agli url di destinazione.

Ma come fa la dispatcher servlet a sapere quali url sono associate ad un certo Controller?

È qui che entra in gioco l’annotazione @RequestMapping

L’annotazione @RequestMapping

Riprendiamo la nostra prima applicazione Spring MVC ed esaminiamo il codice del semplicissimo Controller che avevamo aggiunto al progetto:

@Controller
public class SalutiController {

  @RequestMapping("/")
  public String unSaluto(){
    return "ciao-mondo";
  }
}

Come vedete, avevamo già incontrato l’annotazione @RequestMapping e avevamo accennato alla sua funzione di “marker” per i cosiddetti handler methods all’interno di una classe Controller.

In realtà, lo scopo principale dell’annotazione è quello di definire un’associazione tra un handler e un gruppo di url; l’elemento path dell’annotazione (alias dell’elemento implicito value) serve proprio a specificare quali url saranno associati al metodo.

Per intenderci, nel nostro Controller abbiamo dichiarato un mapping diretto tra il metodo unSaluto e la root dell’applicazione.

C’è da notare che il mapping definito nell’esempio non fa riferimento ad alcun metodo HTTP, difatti esso supporterà richieste di ogni tipo. Se però volessimo esplicitare quali metodi HTTP devono essere gestiti dall’handler, basterà valorizzare l’elemento method.

Ad esempio, se volessimo gestire le sole GET per l’indirizzo / dovremmo utilizzare l’annotazione in questo modo:

@RequestMapping(value="/", method=RequestMethod.GET)

Esistono inoltre alcune specializzazioni di @RequestMapping (introdotte nella versione 4.3 di Spring) che consentono di esprimere i vincoli sui metodi HTTP in modo sicuramente più intuitivo e leggibile. L’annotazione precedente, per esempio, può essere sostituita con:

@GetMapping("/")

Affinare il mapping

A volte capita di avviare un’applicazione Spring MVC e scoprire, tramite una simpatica eccezione, che qualcuno dei nostri Controller presenta un “Ambiguous mapping”.

Specificare url e metodi HTTP, infatti, può non essere sufficiente a identificare in modo preciso quali sono le richieste che dovranno scatenare l’invocazione di un certo handler method.

Fortunatamente, l’annotazione @RequestMapping permette di essere ancor più specifici nella definizione dei criteri di mapping. Vediamo come.

Vincoli sui parametri della richiesta

Valorizzando l’elemento params sarà possibile precisare quali parametri devono essere presenti nella richiesta affinché essa venga mappata correttamente sul metodo annotato.

Il campo params consente d’inserire sia un vincolo di presenza per il parametro:

params = "mio-parametro"

sia restrizioni sul valore dei parametri indicati, utilizzando la seguente sintassi:

params = "mio-parametro=mioValore"

Piccola nota Le espressioni definite per l’elemento params supportano gli operatori di negazione != e !, utili per escludere valori del parametro o negarne la presenza.

Vincoli sugli header della richiesta

Come per i parametri della richiesta, è possibile definire dei vincoli anche sugli header.

L’elemento headers, analogamente a quanto visto per params, permette di specificare quali header sono supportati dal metodo annotato e quali valori sono ammessi per gli stessi:

headers = "mio-header=valore"

Caratteristica interessante di questo elemento è il fatto di supportare l’utilizzo di wildcards (ma solo per i media type). Sarà dunque possibile definire un espressione del tipo:

headers = "content-type=text/*"

Vincoli sui media type

Gli ultimi due elementi che andiamo ad esaminare sono consumes e produces.

Il primo permette di specificare quali sono i media type “consumabili”, ovvero quali formati di dato sono supportati dalla richiesta.

Si possono indicare più media type e wildcard, oppure utilizzare operatori di negazione per escludere eventuali tipi non ammessi, ad esempio:

consumes = {"text/plain", "application/*"}

L’elemento produces, allo stesso modo, consente di mappare solamente le richieste che “producono” determinati media type, ad esempio l’espressione seguente identifica le sole request che producono json con codifica UTF-8:

produces = "application/json; charset=UTF-8"

Mapping a livello di classe

Finora abbiamo considerato il comportamento dell’annotazione @RequestMapping quando applicata ai singoli metodi di una classe; a volte, però, può essere utile applicare determinati criteri di mapping a tutti i metodi di un Controller.

Se stavate già pensando al copia&incolla vi fermo subito, perché l’annotazione @RequestMapping, per nostra fortuna, può essere applicata anche a livello di classe. Questo permette di definire facilmente regole generali di mapping, che se necessario, possono essere estese a livello di metodo:

@Controller
@RequestMapping("/")
public class SalutiController {

  @RequestMapping(method = RequestMethod.GET)
  public String unSaluto() {
    return "ciao-mondo";
  }

  @GetMapping(params = "mio-parametro")
  public String unSalutoConParametro() {
    return "ciao-mondo-param";
  }

  @GetMapping(headers = "mio-header=test")
  public String unSalutoConHeader() {
    return "ciao-mondo-header";
  }
}

Come vedete, nell’esempio precedente ho spostato l’elemento path, con l’url comune a tutti i metodi, a livello di classe. Questo, oltre a rendere più leggibile il codice, elimina inutili ripetizioni e riduce potenziali errori.

Piccola nota Per testare i nuovi metodi aggiunti a SalutiController, in particolare quello che richiede la dichiarazione degli header, vi consiglio di utilizzare un tool come Postman o curl.

In conclusione

La gestione delle richieste HTTP è uno degli aspetti cruciali nella definizione di un Controller. In questo articolo abbiamo visto come Spring MVC permetta di implementare il request mapping in modo flessibile ed efficace grazie all’uso delle Java annotation.

Il framework fa largo uso delle annotazioni per numerose altre funzionalità, alcune delle quali relative agli stessi Controller. Ma di questo parleremo un’altra volta…

Per il momento vi saluto ;-)

Alla prossima,

David