View e i ViewResolver in Spring MVC
Come abbiamo visto nel capitolo precedente i controller delegano alle viste il compito di mostrare all'utente i contenuti in maniera presentabile e stilisticamente appropriata. Quando un controller ritorna una stringa (che non inizi con redirect o forward) o un oggetto ModelAndView
, significa che esso vuole delegare al motore interno di Spring MVC la creazione di una vista identificata dal valore della stringa ritornata (o incapsulata nell'oggetto) sulla base dei parametri inseriti nel Model.
Schematizzando possiamo riassumere che una View identifica una particolare risorsa in grado di mostrare all'utente dei dati (per esempio una classica JSP) mentre un ViewResolver
permette di identificare una determinata View sulla base di una stringa. Spring MVC non presenta nessun ViewResolver
di default. Nel capitolo precedente ottenevamo degli errori ritornando delle stringhe nei controller implementati.
L'InternalResourceViewResolver
Il caso più classico di ViewResolver
è rappresentato dall'InternalResourceViewResolver
che permette di identificare una View sulla base di risorse interne al progetto come ad esempio JSP. Per inserire nel nostro contesto un InternalResourceViewResolver basta creare un bean:
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/view/"/>
<property name="suffix" value=".jsp"/>
</bean>
In questo modo abbiamo definito il nostro ViewResolver che sarà in grado di recuperare la JSP concatenando le stringhe /WEB-INF/view
e .jsp
al valore della stringa ritornata. Il parametro viewClass
rappresenta la classe da cui ciascuna vista verrà istanziata. Se per esempio il nostro controller dovesse tornare home il ViewResolver cercherebbe la JSP in /WEB-INF/view/home.jsp
. Inoltre se nel metodo handler viene istanziato o modificato un oggetto Model
(o ModelAndView
) esso viene passato alla JSP.
Analizziamo un esempio:
@RequestMapping(value="/pagina-personale", method=RequestMethod.GET)
public String paginaPersonale(Model model) {
model.addAttribute("nome", "Alberto");
model.addAttribute("cognome", "Bottarini");
return "pagina-personale";
}
In questo caso i parametri aggiunti all'oggetto Model vengono resi disponibili nella JSP.
<h1>Ciao <c:out value="${nome }" /> <c:out value="${cognome}" /></h1>
Altri esempi di ViewResolver
Oltre all'InternalResourceViewResolver Spring MVC ci mette a disposizione altri Resolver tra cui i
principali sono:
Resolver | Descrizione |
---|---|
XmlViewResolver | Identifica la view corretta a partire da un file XML di configurazione. |
ResourceBoundleViewResolver | Identifica la view corretta a partire da chiavi contenuto nel resource boundle (utile per offrire viste localizzate). |
UrlBasedViewResolver | Identifica la view corretta trasformando il nome logico della vista in un URL (l'InternalResourceViewResolver è una sottoclasse di UrlBasedViewResolver ). |
ContentNavigationViewResolver | Identifica la view sulla base del filename richiesto o in base al parametro Accept della request. |
BeanNameViewResolver | Identifica la view sulla base del nome del bean all'interno del contesto di Spring MVC. |
Il ContentNavigationViewResolver
Uno dei risolver più interessanti è ContentNavigationViewResolver
che rappresenta una sorta di collezione di sotto-resolver ed è in grado di differenziarne l'invocazione sulla base di due parametri:
- contenuto dell'header Accept della request
- estensione del file richiesto
Dato che difficilmente via browser è possibile gestire il valore di Accept, prenderemo in
considerazione la seconda opzione.
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="atom" value="application/atom+xml"/>
<entry key="html" value="text/html"/>
<entry key="json" value="application/json"/>
</map>
</property>
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</list>
</property>
<property name="defaultViews">
<list>
<bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
</list>
</property>
</bean>
Grazie a questo esempio preso direttamente dalla documentazione ufficiale è possibile registrare dei Content-Type
a partire dall'estensione del file (in questo caso atom, html, json) e dei ViewResolver
. Per ciascuna richiesta il ContentNegotiatingViewResolver
identifica il Content-Type
e il ViewResolver
in grado di gestire la chiamata. Nel caso in cui ne trovi uno, delegherà ad esso la gestione, altrimenti si affiderà alla View di default.
Nota: per poter utilizzare questo esempio è necessario importare nel progetto la dipendenza Jackson.
ViewResolver e View personalizzate
Un'applicazione può presentare diverse tipologie di contenuti, quindi diverse viste e quindi diversi ViewResolver
. Tramite una configurazione mirata è possibile gestire ViewResolver multipli, organizzandoli in una sorta di serie ordinata dalla quale Spring MVC cercherà il resolver più adatto. Data la vastissima estendibilità di Spring, è inoltre possibile definire delle View personalizzate definendo puntualmente quale deve essere il loro compito. Nel nostro esempio creeremo una vista che si appoggierà a OpenCSV per generare appunto dei CSV.
Dopo aver incluso le librerie necessarie nel pom.xml
di Maven, creiamo la classe CSVView
:
@Component
public class CSVView extends AbstractView {@Override
protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
CSVWriter writer = new CSVWriter(response.getWriter());
String[] names = (String[]) model.get("names");
writer.writeNext(names);
writer.close();
}
}
Tra gli aspetti importanti è necessario ricordare che la classe deve essere decorata con l'annotation @Component
in modo che Spring la consideri parte del proprio contesto e che deve estendere AbstractView
e quindi implementare il metodo void renderMergedOutputModel
. In questo metodo non facciamo altro che recuperare i dati dal model (che sarà popolato nel controller) e scrivere, grazie alla classe CSVWriter
di OpenCSV, il contenuto della response.
Una volta definita la nostra CSVView è necessario includere nel WebApplicationContext
un ViewResolver
in grado di gestire le nuove classi che verranno istanziate. In questo caso utilizziamo un BeanNameViewResolver
che associerà la CSVView al nome logico CSVView (ricordiamo che i bean identificati dall'annotation @Component
di default acquisiscono come id il nome stesso della classe).
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="1" />
</bean>
Dato che ora nel WebApplicationContext
saranno presenti due diversi View esolver
, è necessario aggiungere in almeno uno di essi la proprietà order per identificarne la priorità. Impostando "1" come order del nuovo BeanNameViewResolver
significherà che Spring MVC cercherà innanzitutto di utilizzare quest'ultimo come resolver e in caso di insuccesso passerà al precedente UrlBasedViewResolver
.
Ora basterà creare un controller che ritornerà CSVView come nome logico della vista:
@RequestMapping(value="/csv", method=RequestMethod.GET)
public String getCSV(Model model) {
String[] names = new String[] { "Alberto", "Luca", "Claudio" };
model.addAttribute("names", names);
return "CSVView";
}