Controladores con anotaciones en Spring MVC Framework

De ChuWiki

En la versión 3 de Spring Web Framework se da prioridad a las anotaciones a la hora de hacer controladores sobre la herencia del controlador de la interfaz Controller y las clases Controladoras que ya venían preparadas en la API de Spring Framework, como SimpleFormController. Veamos aquí algunos conceptos/ejemplos básicos sobre cómo crear controladores con anotaciones.


Configuración de web.xml y springapp-servlet.xml[editar]

Como en cualquier aplicación con Spring Web Framework, debemos configurar nuestro web.xml para que tenga el Servlet propio de Spring Web Framework (org.springframework.web.servlet.DispatcherServlet). Una configuración típica puede ser

<web-app>
  ...
  <servlet>
    <servlet-name>springapp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>springapp</servlet-name>
    <url-pattern>*.htm</url-pattern>
  </servlet-mapping>
  ...
</web-app>

El nombre de nuestro servlet es springapp, puede ser el nombre que nosotros queramos. La clase Servlet es propia de Spring org.springframework.web.servlet.DispatcherServlet y es la que se encargará de recibir las peticiones de los navegadores web y redirigirlas a los controladores que hagamos en nuestro código. El apartado <servlet-mapping> indica qué peticiones del navegador se deben redirigir a qué servlet, en concreto, cualquier petición de un fichero .htm se redirigirá al servlet springapp (o el nombre que hayamos decidido ponerle). Como hemos comentado, el servlet redirigirá estas peticiones a nuestros controladores.

El servlet org.springframework.web.servlet.DispatcherServlet también se encarga de cargar todos los beans de Spring Framework que le indiquemos. Para ello, carga un fichero springapp-servlet.xml situado en el directorio WEB-INF, junto a web.xml y carga los beans que se indiquen en ese fichero. El nombre del fichero debe ser el nombre del servlet (springapp en nuestro ejemplo) seguido de -servlet.xml.

Si queremos usar controladores con anotaciones, en este fichero springapp-servlet.xml debemos indicar dónde están las clases que hace de controlador y que llevan las anotaciones correspondientes. El contenido del fichero springapp-servlet.xml puede ser como el siguiente

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <context:component-scan base-package="com.chuidiang.ejemplo_spring"/>
    
</beans>

Hay que fijarse que en la parte de xsi:schemaLocation de <beans> hemos añadido dos líneas para context y spring-context-2.5.xsd (aparte de las dos "estándar" de beans y spring-beans). Esto nos permite usar el tag <context:component-scan> en donde se indica el paquete java a partir del cual buscar clases con anotación de controladores.


El controlador[editar]

Tal cual lo tenemos configurado, cuando hagamos desde el navegador una petición de un fichero .htm, el Servlet org.springframework.web.servlet.DispatcherServlet tratará de redirigir la llamada a uno de nuestros controladores. Para hacer dicho controlador, tenemos que hacer una clase java con la anotación @Controller, ya que hemos elegido hacerlo con anotaciones en clases dentro del paquete com.chuidiang.ejemplo_spring, como indica el fichero springapp-servlet.xml

La clase Controlador comenzará más o menos de esta forma

package com.chuidiang.ejemplo_spring;

import org.springframework.stereotype.Controller;

@Controller
public class Controlador {
...

El siguiente paso es decidir exactamente a qué método de nuestro controlador queremos que se llame en función del página htm pedida. Para ello nos basta con poner un método cualquiera en la clase con la anotacion @RequestMapping. Bueno, en realidad el método no puede ser de cualquier forma, tiene que ser un método que admita ninguno, uno o más de determinados parámetros y que devuelva uno de los tipos que luego entenderá el Framework de Spring. El siguiente enlace RequestMapping muestra un listado de todos los posibles tipos de parámetros que puede admitir el método y los posibles tipos de resultados. No vamos a ver aquí todas las combinaciones posibles, simplemente unos cuantos ejemplos más o menos habituales.

Uno de los más habituales es que admita como parámetro un ModelMap y devuelva un String.

	@RequestMapping("uno.htm")
	public String uno(ModelMap modelo) {
		modelo.addAttribute("uno", "1");
		return "index.jsp";
	}

La anotación @RequestMapping debe llevar entre comillas la página que debe teclear el usuario para que se llame a este método. Si nuestra aplicación está en un fichero aplicacion.war y lo hemos desplegado en apache tomcat (u otro servidor de servlets), la siguiente url

http://nuestroservidor:8080/aplicacion/uno.htm

provocará la llamada a nuestro método uno() del controlador. Hay que fijarse que en el fichero web.xml habíamos puesto que se enviaran al servlet org.springframework.web.servlet.DispatcherServlet las peticiones de páginas .htm. Será el servlet el que al ver que se pide uno.htm se encargue de llamar al método uno() del controlador.

El método uno() del controlador devuelve en este caso un String (uno de los tipos admitidos para los métodos que anotación RequestMapping. En este String debemos indicar cual es la página jsp que se debe mostrar. Así, el servlet, después de llamar al controlador, llamará a la página jsp que devolvamos en el método, en nuestro caso index.jsp ( http://nuestroservidor:8080/aplicacion/index.jsp )


Pasar datos a la vista[editar]

La gracia de usar un controlador consiste en el parámetro ModelMap. En ese parámetro, que en el fondo es como un HashMap, podemos meter objetos java identificados por una clave y que estarán accesibles desde la página jsp. De esta forma, podemos visualizar unos datos u otros en la página jsp. En el ejemplo, hemos metido dentro de ModelMap un objeto String de valor "1" identificado por la clave "uno". Este valor estará accesible desde index.jsp y podemos acceder a él así

<%@ page session="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
...
<h2>uno =  <c:out value="${uno}" /></h2>

Por supuesto, en un ejemplo más complejo el controlador podría cargar datos de una base de datos, meter una lista (List<Dato>) dentro del ModelMap y nuestro index.jsp podría mostrar una

con todos esos datos. Y por supuesto, no tenemos por qué meter solo un objeto java, podemos meter todos los que queramos, cada uno con su clave propia.

Datos compartidos por todos los métodos[editar]

Es posible que nuestro controlador tenga varios métodos, atendiendo de esta forma a varias URLs, y es posible también que en todas ellas nos interese pasar un mismo dato a la vista, siempre el mismo. Puesto que sería un poco "rollo" tener que rellenar el ModelMap en todos los métodos siempre igual, podemos hacer un método especial con la anotación @ModelAttribute, de esta forma

@ModelAttribute("dato")
public UnBean unMetodoCualquiera() {
   UnBean bean = new UnBean();
   bean.setTexto("inicial"); // suponiendo que unBean tenga método setTexto(String)...
   return bean;
}

es decir, un método con la anotación @ModelAttribute con el nombre que queremos que quede etiquetado el dato en cuestión ("dato" en este caso). El método no recibe parámetros y devuelve el dato que queremos pasar a la vista, de cualquier clase que queramos. Sólo tenemos que rellenar el dato y hacer un return de él.

Spring llamará a todos los métodos con anotación @ModelAttribute antes de hacer la llamada definitiva al método @RequestMapping que toque en función de la URL. De esta forma, cualquier llamada a este controlador pasará a la vista el "dato" con "UnBean" relleno dentro.

Datos de sesión[editar]

La "pega" del método anterior es que siempre nos devolverá el dato inicializado según lo inicialicemos en el método. A veces nos interesa tener datos de sesión, que se mantengan mientras navegamos de una página a otra del controlador y que podamos cambiar sólo si nos interesa. Para conseguir esto, en la clase ponemos la anotación @SessionAttributes(), indicando qué datos queremos que se mantengan en sesión

@Controller
@SessionAttributes({"datoSesion1","datoSesion2"})
public class Controlador {
   ...

En este ejemplo, los datos "datoSesion1" y "datoSesion2" serán de sesión y mantendrán sus valores mientras navegamos por las páginas del controlador. En principio estos datos no están definidos, pero podemos definirlos en cualquier método

@RequestMapping("uno.htm")
public String uno(ModelMap modelo) {
   modelo.addAttribute("uno", "1");
   modelo.addAttribute("datoSesion1","uno");
   return "index.jsp";
}

Y ahí permanecerá ese dato, disponible en el modelo que se pasa a todos los métodos del controlador, hasta que otro método del controlador lo borre o lo modifique.

Ojo, no hay que definir datos de sesión en los métodos con anotación @ModelAttribute, puesto que a estos métodos se los llama siempre que hay una petición a nuestro controlador y el dato de sesión acabará con un valor fijo, el que le de ese método. Tampoco tiene sentido hacerlo que el dato sea de sesión si lo vamos a definir en un método anotado con @ModelAttribute, porque siempre se llama a este método y va a definirlo y estar disponible para todos los demás métodos, sea o no de sesión.

En el momento que nos interese, desde un método del controlador podemos limpiar los datos de sesión. Para ello, el método debe recibir un atributo SessionStatus y debemos llamar a su método setComplete()

@RequestMapping("dos.htm")
public String dos(SessionStatus status, ModelMap modelo) {
   status.setComplete();
   return "index.jsp";
}

Es importante notar que este método no borra el dato de ModelMap en el momento, cuando se llama a este método dos(), la página index.jsp de retorno seguirá teniendo el dato disponible en ModelMap. Es cuando salimos de esa página a la siguiente cuando el dato deja de estar disponible ... hasta que alguien vuelva a definirlo. Podemos borrarlo si nos interesa con modelo.remove()


@RequestMapping("dos.htm")
public String dos(SessionStatus status, ModelMap modelo) {
   status.setComplete();
   modelo.remove("datoSesion1");
   return "index.jsp";
}

Paso de parámetros request al controlador[editar]

Los métodos del controlador pueden recibir parámetros de una petición del navegador, bien sea por método POST o por método GET. La forma de hacerlo sería poner en nuestro controlador un método de esta forma

@RequestMapping(value = "tres.htm")
public String tres(@RequestParam("texto") String texto,  ModelMap modelo) {
   ...      

de esta forma, recibiremos un parámetro de nombre texto si hacemos una llamada a nuestro método con esta url

http://nuestroservidor:8080/aplicacion/tres.htm?texto=hola

que sería en forma GET o bien, si enviamos desde un formulario con POST

<form method="post" action="tres.htm">
   <input type="text" name="texto"/>
   <input type="submit" value="OK"/>
</form>

Si queremos que el método sólo atienda GET o POST, podemos indicarlo en la anotación del método del controlador

@RequestMapping(value = "tres.htm", method = RequestMethod.POST)
public String tres(@RequestParam("texto") String texto,  ModelMap modelo) {
    ...