Skip to content

4. Acciones: el modelo

Volvamos a la arquitectura de una aplicación Spring MVC:

En el capítulo anterior, analizamos el proceso que lleva la solicitud [1] al controlador y a la acción [2a] que la procesarán, un mecanismo que se conoce como enrutamiento. Además, presentamos las diferentes respuestas que una acción puede enviar al navegador. Hasta ahora hemos presentado acciones que no aprovechaban la solicitud que se les presentaba. Una solicitud [1] lleva consigo diversa información que Spring MVC presenta a la acción en forma de un modelo. No hay que confundir este término con el modelo M de una vista V [2c] que es generada por la acción:

  • la solicitud HTTP del cliente llega a [1];
  • en [2], la información contenida en la solicitud se transformará en el modelo de acción [3] —una clase, a menudo, aunque no necesariamente, que servirá como entrada para la acción [4]—;
  • en [4], la acción, a partir de este modelo, generará una respuesta. Esta tendrá dos componentes: una vista V [6] y el modelo M de esta vista [5];
  • la vista V [6] utilizará su modelo M [5] para generar la respuesta HTTP destinada al cliente.

En el modelo MVC, la acción [4] forma parte del C (controlador), el modelo de la vista [5] es el M y la vista [6] es el V.

Este capítulo analiza los mecanismos de vinculación entre la información transportada por la solicitud, que por naturaleza son cadenas de caracteres, y el modelo de la acción, que puede ser una clase con propiedades de diversos tipos.

Nota: el término [Modèle d'action] no es un término reconocido.

Creamos un nuevo controlador para estas nuevas acciones:

  

El controlador [ActionModelController] será, por el momento, el siguiente:


package istia.st.springmvc.controllers;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class ActionModelController {

}
  • línea 5: cabe recordar que la anotación [@RestController] hace que la respuesta enviada al cliente sea la serialización como cadena de caracteres del resultado de las acciones del controlador;

4.1. [/m01]: parámetros de un GET

Agregamos la siguiente acción [/m01]:



    // ----------------------- recuperar parámetros con GET------------------------
    @RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m01(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • línea 4: la acción admite dos parámetros denominados [nom] y [age]. Se inicializarán con los parámetros que llevan esos mismos nombres en la consulta HTTP GET;

Los resultados en Chrome para [1-3] son los siguientes:

  • en [1], la consulta GET con los parámetros [nom] y [age];
  • en [3], se observa que la acción [/m01] sí recuperó estos parámetros;

4.2. [/m02]: parámetros de un POST

Agregamos la siguiente acción [/m02]:



    // ----------------------- recuperar parámetros con POST------------------------
    @RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m02(String nom, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", nom, age);
}
  • línea 4: la acción admite dos parámetros llamados [nom] y [age]. Se inicializarán con los parámetros que llevan esos mismos nombres en la consulta HTTP POST;

Los resultados con [Advanced rest Client] son los siguientes:

  • en [1-3], la consulta POST con los parámetros [nom] y [age];
  • en [4-5], se establece el encabezado HTTP [Content-Type] de la consulta POST. Debe ser [Content-Type: application/x-www-form-urlencoded];
  • en [6], [Form Data] proporciona la lista de parámetros de una operación POST. Aquí se ven los parámetros [nom] y [age];
  • en [7], la respuesta del servidor que muestra que la acción [/m02] recuperó correctamente los parámetros [nom] y [age]; ;

4.3. [/m03]: parámetros con los mismos nombres

En el apartado 2.5.2.8 vimos que la lista de selección múltiple podía enviar al servidor parámetros con los mismos nombres. Veamos cómo una acción puede recuperarlos. Agregamos la siguiente acción [/m03]:


    // ----------------------- recuperar parámetros con los mismos nombres-----------------
    @RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m03(String nom[]) {
        return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", nom));
}
  • línea 2: la acción admite un parámetro llamado [nombre[]]. Aquí se inicializará con todos los parámetros que tengan ese nombre, ya sea en un GET o en un POST, ya que aquí no se ha especificado el tipo de la solicitud;

Los resultados son los siguientes:

  • mediante un POST y un [1], se envían los parámetros de un [2];
  • también se incluyen parámetros en el URL y el [3];
  • en el [4], los cuatro parámetros con el mismo nombre [nom]: [Query String parameters] son los parámetros de URL, [Form Data] son los parámetros enviados;
  • en [5], se observa que la acción [/m03] recuperó los cuatro parámetros denominados [nom];

4.4. [/m04]: asignar los parámetros de la acción a un objeto Java

Supongamos la siguiente nueva acción [/m04]:


    // ------ asignar los parámetros a un objeto (Command Object) ---------------
    @RequestMapping(value = "/m04", method = RequestMethod.POST)
    public Personne m04(Personne personne) {
        return person;
}
  • línea 3: la acción tiene como parámetro una persona del siguiente tipo:

public class Personne {

    // identificador
    private Integer id;
    // nombre
    private String nom;
    // edad
    private int age;
....
    // getters y setters
...
}
  • Para crear el parámetro [Personne personne], Spring MVC crea un [new Personne()];
  • luego, si hay parámetros con los nombres de los campos [id, nom, age] del objeto creado, lo instancia con los campos mediante sus setters;
  • línea 4: la acción devuelve un tipo [Personne] que, por lo tanto, se serializará como una cadena de caracteres antes de enviarse al cliente. Hemos visto que, por defecto, la serialización realizada es una serialización jSON. Por lo tanto, el cliente debería recibir la cadena jSON de una persona;

He aquí un ejemplo:

  • en [1], los parámetros [id, nom, age] para construir un objeto [Personne];
  • en [2], la cadena jSON de esa persona;

¿Qué pasa si no se envían todos los campos de una persona? Probemos:

  • en [2], solo se ha inicializado el parámetro [id];

4.5. [/m05]: recuperar los elementos de un URL

O bien la siguiente nueva acción [/m05]:


    // ----------------------- recuperar los elementos del URL ------------------------
    @RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("a", a);
        map.put("b", b);
        return map;
}
  • línea 2: el URL procesado tiene la forma [/m05/{a}/x/{b}], donde {param} es un elemento parámetro del URL;
  • línea 3: los elementos de parámetro del URL se recuperan con la anotación [@PathVariable];
  • líneas 4-6: los elementos [a] y [b] recuperados se colocan en un diccionario;
  • línea 7: la respuesta será la cadena jSON de ese diccionario;

Los resultados son los siguientes:

 

4.6. [/m06]: recuperar elementos de URL y parámetros

Sea la siguiente nueva acción [/m06]:


    // -------- recuperar elementos de URL y parámetros---------------
    @RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("a", a);
        map.put("b", b);
        map.put("c", c);
        return map;
}
  • línea 3: se recuperan tanto elementos de URL y [Integer a, Double b] como un parámetro (GET o POST) de [Double c];
  • líneas 4-7: estos elementos se colocan en un diccionario;
  • línea 8: que forma la respuesta del cliente, quien recibirá, por lo tanto, la cadena jSON de este diccionario;

Estos son los resultados:

 

Cabe destacar la barra (/) al final de la ruta [http://localhost:8080/m06/100/x/200.43/]. Sin ella, se obtiene el siguiente resultado incorrecto:

 

4.7. [/m07]: acceder a la consulta completa

Consideremos la siguiente acción nueva [/m07]:


    // ------ acceder a la consulta HttpServletRequest ------------------------
    @RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m07(HttpServletRequest request) {
        // las cabeceras de HTTP
        Enumeration<String> headerNames = request.getHeaderNames();
        StringBuffer buffer = new StringBuffer();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
        }
        return buffer.toString();
}
  • línea 3: se le pide a Spring MVC que inyecte el objeto [HttpServletRequest request], el cual encapsula toda la información que se puede obtener de la solicitud;
  • líneas 5-10: se recuperan todos los encabezados HTTP de la solicitud para ensamblarlos en una cadena de caracteres que se envía al cliente (línea 11);

Los resultados son los siguientes:

  • en [1], los encabezados HTTP de la consulta;
  • en [2], la respuesta. Aquí se encuentran todos los encabezados HTTP de la solicitud.

4.8. [/m08]: acceso al objeto [Writer]

Consideremos la siguiente acción:


    // ----------------------- inyección de writer ------------------------
    @RequestMapping(value = "/m08", method = RequestMethod.GET)
    public void m08(Writer writer) throws IOException {
        writer.write("Bonjour le monde !");
}
  • línea 3: Spring MVC inyecta el objeto [Writer writer], que permite escribir en el flujo de la respuesta al cliente;
  • línea 3: la acción devuelve un tipo [void], lo que indica que debe construir por sí misma la respuesta al cliente;
  • línea 4: se agrega un texto al flujo de la respuesta al cliente;

Los resultados son los siguientes:

  • en [2], se observa que el encabezado HTTP [Content-Type] no se envió;
  • en [3], la respuesta;

4.9. [/m09]: acceder a un encabezado HTTP

Consideremos la siguiente acción:


    // ----------------------- inyección de RequestHeader ------------------------
    @RequestMapping(value = "/m09", method = RequestMethod.GET)
    public String m09(@RequestHeader("User-Agent") String userAgent) {
        return userAgent;
}
  • línea 3: la anotación [@RequestHeader("User-Agent")] permite recuperar el encabezado HTTP [User-Agent];
  • línea 4: se muestra el texto de este encabezado;

Los resultados son los siguientes:

  • en [2], la cabecera HTTP [User-Agent];
  • en [3], la acción [/m08] recuperó correctamente este encabezado;

Una cookie es, por lo general, un encabezado HTTP que el:

  • servidor envía por primera vez al cliente;
  • el cliente devuelve sistemáticamente al servidor;

Primero creemos una acción que genere la cookie:


    // ----------------------- creación de Cookie ------------------------
    @RequestMapping(value = "/m10", method = RequestMethod.GET)
    public void m10(HttpServletResponse response) {
        response.addCookie(new Cookie("cookie1", "remember me"));
}
  • línea 3: inyectamos el objeto [HttpServletResponse response] para tener control total sobre la respuesta;
  • línea 4: creamos una cookie con una clave [cookie1] y un valor [remember me] (Nota: los caracteres acentuados en el valor de una cookie provocan errores);
  • línea 3: la acción no devuelve nada. Además, no escribe nada en el cuerpo de la respuesta. Por lo tanto, el cliente recibirá un documento vacío. La respuesta solo se utiliza para agregarle el encabezado HTTP de una cookie;

Veamos los resultados:

  • en [1]: la solicitud;
  • en [2]: la respuesta está vacía;
  • en [3]: la cookie creada por la acción;

Ahora creemos una acción para recuperar esta cookie que el navegador enviará a partir de ahora con cada solicitud:


    // ----------------------- inyección de cookie ------------------------
    @RequestMapping(value = "/m11", method = RequestMethod.GET)
    public String m10(@CookieValue("cookie1") String cookie1) {
        return cookie1;
}
  • línea 3: la anotación [@CookieValue("cookie1")] permite recuperar la cookie de clave [cookie1];
  • línea 4: este valor será la respuesta que se le dará al cliente;

Veamos los resultados:

  • en [2], vemos que el navegador devuelve la cookie;
  • en [3], la acción la ha recuperado correctamente;

4.11. [/m12]: acceder al cuerpo de un POST

Los parámetros enviados suelen ir acompañados del encabezado HTTP [Content-Type: application/x-www-form-urlencoded]. Se puede acceder a toda la cadena enviada. Creamos la siguiente acción:


    // ----------- recuperar el cuerpo de un POST de tipo cadena------------------------
    @RequestMapping(value = "/m12", method = RequestMethod.POST)
    public String m12(@RequestBody String requestBody) {
        return requestBody;
}
  • línea 3: la anotación [@RequestBody] permite recuperar el cuerpo de POST. Aquí, suponemos que este es de tipo [String];
  • línea 4: se devuelve este cuerpo al cliente;

A continuación se muestra un primer ejemplo:

  • en [2], los valores enviados;
  • en [3], el encabezado HTTP [Content-Type] de la solicitud;
  • en [4], la respuesta del servidor;

Los parámetros enviados no siempre tienen el formato simple [p1=v1&p2=v2] que hemos utilizado con frecuencia hasta ahora. Veamos un caso más complejo:

  • en [2-3]: se ingresan los valores enviados en el formato [clé:value];
  • en [5], la cadena que se publicó;

Con el tipo [Content-Type: application/x-www-form-urlencoded], la cadena que se envía debe tener el formato [p1=v1&p2=v2]. Si se desea enviar cualquier cosa, se utilizará el tipo [Content-Type: text/plain]. A continuación se muestra un ejemplo:

  • en [2-3], se crea el encabezado HTTP [Content-Type]. Por defecto, [5]; este es el que se utilizará en lugar del definido en [6]. El atributo [charset=utf-8] es importante. Sin él, se pierden los caracteres acentuados de la cadena enviada;
  • en [4], la cadena enviada se recupera correctamente en [7];

4.12. [/m13, /m14]: recuperar valores enviados en jSON

Es posible enviar parámetros con el encabezado HTTP [Content-Type: application/json]. Creamos la siguiente acción:


    // ----------------------- recuperar el cuerpo jSON de un POST
    @RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
    public String m13(@RequestBody Personne personne) {
        return personne.toString();
}
  • línea 2: [consumes = "application/json"] especifica que la acción espera un cuerpo jSON;
  • línea 3: [@RequestBody] representa ese cuerpo. Esta anotación se ha asociado a un objeto de tipo [Personne]. El cuerpo jSON se deserializará automáticamente en este objeto;
  • línea 4: se utiliza el método [Personne].toString() para devolver algo que no sea la cadena jSON enviada;

A continuación, un ejemplo:

  • en [2], la cadena jSON enviada;
  • en [3], el [Content-Type] de la solicitud;
  • en [4], la respuesta del servidor;

Se puede hacer lo mismo de otra manera:


    // ----------------------- recuperar el cuerpo jSON de un POST 2 -------------------
    @RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
    public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
        Personne personne = new ObjectMapper().readValue(requestBody, Personne.class);
        return personne.toString();
}
  • línea 2: se indicó que el método esperaba un flujo de tipo [text/plain]. Spring MVC procesará entonces el cuerpo de la solicitud como un tipo [String] (línea 3);
  • línea 4: se deserializa la cadena jSON en un objeto [Personne] (véase el párrafo 9.7, página 542);

Los resultados son los siguientes:

  • en [3], debe ser [text/plain];

4.13. [/m15]: recuperar la sesión

Volvamos a la arquitectura de ejecución de una acción:

La clase del controlador se instancia al inicio de la solicitud del cliente y se destruye al final de la misma. Por lo tanto, no puede utilizarse para almacenar datos entre dos solicitudes, aunque se llame repetidamente. Es posible que se desee almacenar dos tipos de datos:

  • datos compartidos por todos los usuarios de la aplicación web. Por lo general, se trata de datos de solo lectura;
  • datos compartidos por las solicitudes de un mismo cliente. Estos datos se almacenan en un objeto llamado «Sesión». En este caso, se habla de «sesión del cliente» para referirse a la memoria del cliente. Todas las solicitudes de un cliente tienen acceso a esta sesión. Pueden almacenar y leer información en ella.

Arriba, mostramos los tipos de memoria a los que tiene acceso una acción:

  • la memoria de la aplicación, que en la mayoría de los casos contiene datos de solo lectura y a la que tienen acceso todos los usuarios;
  • la memoria de un usuario específico, o sesión, que contiene datos de lectura y escritura y a la que pueden acceder las solicitudes sucesivas de un mismo usuario;
  • aunque no se muestra arriba, existe una memoria de solicitud, o contexto de solicitud. La solicitud de un usuario puede ser procesada por varias acciones sucesivas. El contexto de la solicitud permite que una acción 1 transmita información a una acción 2.

Veamos un primer ejemplo que ilustra estas diferentes memorias:


    // ----------------------- recuperar la sesión ------------------------
    @RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m15(HttpSession session) {
        // se recupera el objeto de clave [compteur] de la sesión
        Object objCompteur = session.getAttribute("compteur");
        // se convierte en un entero para incrementarlo
        int iCompteur = objCompteur == null ? 0 : (Integer) objCompteur;
        iCompteur++;
        // se vuelve a colocar en la sesión
        session.setAttribute("compteur", iCompteur);
        // se devuelve como resultado de la acción
        return String.valueOf(iCompteur);
}

Spring MVC mantiene la sesión del usuario en un objeto de tipo [HttpSession].

  • línea 3: se le pide a Spring MVC que inyecte el objeto [HttpSession] en los parámetros de la acción;
  • línea 5: se recupera de ahí un atributo llamado [compteur]. Una sesión se comporta como un diccionario, un conjunto de pares [clé, valeur]. Si la clave [compteur] no existe en la sesión, se recupera un puntero null;
  • línea 7: el valor asociado a la clave [compteur] será de tipo [Integer];
  • línea 8: incremento del contador;
  • línea 10: actualización del contador en la sesión;
  • línea 12: el valor del contador se envía al cliente;

Cuando se ejecute [/m15] por:

  • primera vez, en la línea 12, el contador tendrá el valor 1;
  • la segunda vez, en la línea 5, se recuperará ese valor 1 para cambiarlo a 2;
  • ...

A continuación se muestra un ejemplo de ejecución:

  • en [1], se obtiene correctamente el primer valor del contador;
  • en [2], el servidor ha enviado una cookie de sesión. Tiene la clave [JSESSIONID] y, como valor, una cadena de caracteres única para cada usuario. Recordemos que el navegador reenvía sistemáticamente las cookies que recibe. Así, cuando solicitemos la acción [/m15] por segunda vez, el cliente reenviará esta cookie, lo que permitirá al servidor reconocerlo y vincularlo a su sesión. De esta manera se mantiene la memoria del usuario;

Veamos la segunda solicitud:

  • En [3], vemos que el cliente reenvía la cookie de sesión. Se puede observar que en la respuesta del servidor ya no aparece esta cookie de sesión. Ahora es el cliente quien la envía para que lo reconozcan;
  • en [4], el segundo valor del contador. Efectivamente, se ha incrementado;

4.14. [/m16]: recuperar un objeto del ámbito [session]

Es posible que queramos colocar todos los datos de la sesión de un usuario en un único objeto y guardar únicamente este en la sesión. Seguimos este enfoque. Colocamos el contador en el siguiente objeto [SessionModel]:

  

package istia.st.sprinmvc.models;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {

    private int compteur;

    public int getCompteur() {
        return compteur;
    }

    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }

}
  • línea 7: la anotación [@Component] es una anotación de Spring (línea 5) que convierte a la clase [SessionModel] en un componente cuyo ciclo de vida es administrado por Spring;
  • línea 8: la anotación [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] también es una anotación de Spring (líneas 3-4). Cuando Spring MVC la detecta, se crea la clase correspondiente y se coloca en la sesión del usuario. El atributo [proxyMode = ScopedProxyMode.TARGET_CLASS] es importante. Gracias a él, Spring MVC crea una instancia por usuario y no una única instancia para todos los usuarios (singleton);
  • línea 11: el contador;

Para que este nuevo componente de Spring sea reconocido, hay que verificar la configuración de la aplicación en la clase [Application]:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • línea 9: los componentes de Spring se buscan en el paquete [istia.st.springmvc.controllers]. Esto ya no es suficiente. Modificamos esta línea de la siguiente manera:

@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })

Hemos agregado el paquete donde se encuentra la clase [SessionModel].

Ahora, agregamos la siguiente acción:


    @Autowired
    private SessionModel session;
    
    // ------ gestionar un objeto de ámbito (scope) de sesión [Autowired] -----------
    @RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m16() {
        session.setCompteur(session.getCompteur() + 1);
        return String.valueOf(session.getCompteur());
}
  • líneas 1-2: el componente Spring [SessionModel] se inyecta como [@Autowired] en el controlador. Recordemos aquí que un controlador de Spring es un singleton. Por lo tanto, resulta paradójico inyectar en él un componente de alcance menor, en este caso de alcance [Session]. Ahí es donde entra en juego la anotación [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] del componente [SessionModel]. Cada vez que el código del controlador accede al campo [session] de la línea 2, se ejecuta un método proxy para devolver la sesión de la solicitud que el controlador está procesando en ese momento;
  • línea 6: ya no se necesita el objeto [HttpSession] en los parámetros de la acción;
  • línea 7: se recupera o se incrementa el contador;
  • línea 8: se devuelve su valor;

A continuación se muestra un ejemplo de ejecución:

La primera vez

La segunda vez

Ahora, tomemos otro navegador que representará a un segundo usuario. En este caso, utilizaremos el navegador Opera:

Como se ve arriba en [1], este segundo usuario obtiene un valor de contador de 1. Esto demuestra que su sesión y la del primer usuario son diferentes. Si observamos los intercambios entre el cliente y el servidor (Ctrl-Mayús-I también en Opera), vemos en [2] que este segundo usuario tiene una cookie de sesión diferente a la del primer usuario. Esto es lo que garantiza la independencia de las sesiones.

4.15. [/m17]: recuperar un objeto del ámbito de [application]

Volvamos a la arquitectura de ejecución de una acción:

Ya sabemos cómo construir la sesión del usuario. Ahora vamos a construir un objeto de ámbito [application] cuyo contenido será de solo lectura y accesible para todos los usuarios. Introducimos la clase [ApplicationModel], que será el objeto de ámbito [application]:

 

package istia.st.springmvc.models;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Component;

@Component
public class ApplicationModel {

    // contador
    private AtomicLong compteur = new AtomicLong(0);

    // getters y setters
    public AtomicLong getCompteur() {
        return compteur;
    }

    public void setCompteur(AtomicLong compteur) {
        this.compteur = compteur;
    }

}
  • línea 5: la anotación [@Component] hace que la clase [ApplicationModel] sea un componente administrado por Spring. La naturaleza predeterminada de los componentes de Spring es el tipo [singleton]: el componente se crea como un único ejemplar cuando se instancia el contenedor de Spring, es decir, por lo general al iniciar la aplicación. Podemos aprovechar este ciclo de vida para almacenar en el singleton información de configuración a la que tendrán acceso todos los usuarios;
  • línea 11: un contador de tipo [AtomicLong]. Este tipo cuenta con un método denominado [incrementAndGet], que se considera atómico. Esto significa que un hilo que ejecuta este método tiene la garantía de que ningún otro hilo leerá el valor del contador (Get) entre el momento en que el primer hilo lo lee (Get) y lo incrementa (increment), lo que provocaría errores, ya que dos hilos leerían el mismo valor del contador y, en lugar de incrementarse en dos, se incrementaría en uno;

Creamos la siguiente acción nueva [/m17]:


@Autowired
    private ApplicationModel application;

    // ----- gestionar un objeto de ámbito de aplicación [Autowired] ------------------------
    @RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m17() {
        return String.valueOf(application.getCompteur().incrementAndGet());
    }
  • líneas 1-2: inyectamos el componente [ApplicationModel] en el controlador. Es un singleton. Por lo tanto, cada usuario tendrá una referencia al mismo objeto;
  • línea 7: devolvemos el contador de alcance [application] después de incrementarlo;

Aquí hay dos ejemplos, uno con Chrome y otro con Opera:

En el ejemplo anterior, se observa que ambos navegadores han trabajado con el mismo contador, lo cual no ocurría con la sesión. Estos dos navegadores representan a dos usuarios diferentes que tienen acceso a los datos del ámbito [application]. En general, se debe evitar incluir información de lectura/escritura en los objetos de ámbito [application], tal como se hizo anteriormente con el contador. De hecho, los subprocesos de ejecución de los distintos usuarios acceden al mismo tiempo a los datos del ámbito [application]. Si hay información de escritura, es necesario sincronizar los accesos de escritura, tal como se hizo anteriormente con el tipo [AtomicLong]. Los accesos concurrentes son fuente de errores de programación. Por lo tanto, es preferible incluir únicamente información de solo lectura en los objetos de ámbito [application].

4.16. [/m18]: recuperar un objeto del ámbito [session] con [@SessionAttributes]

Existe otra forma de recuperar información del ámbito [session]. Vamos a poner en sesión el siguiente objeto:


package istia.st.springmvc.models;

public class Container {
    // el contador
    public int compteur=10;

    // los getters y setters
    public int getCompteur() {
        return compteur;
    }

    public void setCompteur(int compteur) {
        this.compteur = compteur;
    }
}

Vamos a utilizar este objeto con las dos acciones siguientes:


    // uso de [@SessionAttribute] ----------------------
    @RequestMapping(value = "/m18", method = RequestMethod.GET)
    public void m18(HttpSession session) {
        // aquí se introduce la clave [container] en la sesión
        session.setAttribute("container", new Container());
    }

    // uso de [@ModelAttribute] ----------------------
    // Aquí se insertará la clave [container] de la sesión
    @RequestMapping(value = "/m19", method = RequestMethod.GET)
    public String m19(@ModelAttribute("container") Container container) {
        container.setCompteur(1 + container.getCompteur());
        return String.valueOf(container.getCompteur());
    }
  • líneas 3-6: la acción [/m18] no devuelve ningún resultado. Solo sirve para crear un objeto en la sesión con la clave [container];
  • línea 11: en la acción [/m19], se utiliza la anotación [@ModelAttribute]. El comportamiento de esta anotación es bastante complejo. El parámetro [container] de esta anotación puede referirse a varias cosas y, en particular, a un objeto de la sesión. Para ello, es necesario que este haya sido declarado con una anotación [@SessionAttributes] en la propia clase:

@RestController
@SessionAttributes({"container"})
public class ActionModelController {
  • la línea 2 anterior indica que la clave [container] forma parte de los atributos de la sesión;

Resumamos:

  • en [/m18], la clave [container] se incorpora a la sesión;
  • la anotación [@SessionAttributes({"container"})] permite que esta clave se pueda inyectar en un parámetro anotado con [@ModelAttribute("container")];
  • aunque no se ve en el ejemplo de ejecución que sigue, una información anotada con [@ModelAttribute] forma parte automáticamente del modelo M transmitido a la vista V;

A continuación se muestra un ejemplo de ejecución. En primer lugar, se ingresa la clave [container] en la sesión mediante la acción [/m18] [1]. Luego, se llama dos veces a la acción [/m19] para ver cómo se incrementa el contador.

4.17. [/m20-/m23]: inserción de información con [@ModelAttribute]

Consideremos la siguiente acción nueva:


    // el atributo p formará parte de todas las plantillas de vista [Model] ----------------
    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
    }

    // ---------------instanciación de @ModelAttribute --------------------------
    // se inyectará si está en la sesión
    // se inyectará si el controlador ha definido un método para este atributo
    // puede provenir de los campos de URL si existe un convertidor de String al tipo del atributo
    //; de lo contrario, se construye con el constructor predeterminado
    //; luego, los atributos del modelo se inicializan con los parámetros de GET o de POST
    // el resultado final formará parte del modelo generado por la acción
    
    // el atributo p se inyecta en los argumentos------------------------
    @RequestMapping(value = "/m20", method = RequestMethod.GET)
    public Personne m20(@ModelAttribute("p") Personne personne) {
        return personne;
}
  • líneas 2-5: definen un atributo de modelo llamado [p]. Se trata del modelo M de una vista V, modelo representado por un tipo [Model] en Spring MVC. Un modelo se comporta como un diccionario de pares [clé, valeur]. Aquí, la clave [p] está asociada al objeto [Personne] creado por el método [getPersonne]. El nombre del método puede ser cualquiera;
  • línea 17: el atributo de modelo de clave [p] se inyecta en los parámetros de la acción. Esta inyección se realiza según las reglas de las líneas 8 a 12. En este caso, nos encontramos en la situación definida en la línea 9. Por lo tanto, en la línea 17, el parámetro [Personne personne] será el objeto [Personne(7,'abcd',14)];
  • línea 18: se devuelve el objeto [personne] para su verificación. Este se serializará como jSON antes de enviarse al cliente.

A continuación, un ejemplo:

 

Ahora, analicemos la siguiente acción:


    // --------- el atributo p forma parte automáticamente de la plantilla M de la vista V
    @RequestMapping(value = "/m21", method = RequestMethod.GET)
    public String m21(Model model) {
        return model.toString();
}

Una acción que desea mostrar una vista V debe construir el modelo M de esta. Spring MVC maneja este modelo con un tipo [Model] que puede inyectarse en los parámetros de la acción. Inicialmente, este modelo está vacío o contiene la información etiquetada con la anotación [@ModelAttribute]. La acción puede enriquecer o no este modelo antes de transmitirlo a una vista.

  • línea 3: inserción del modelo M;
  • línea 4: queremos ver qué hay dentro. Lo serializamos como una cadena de caracteres para enviarlo al cliente. Aquí se utilizará el método [Personne.toString]. Por lo tanto, debe existir;

A continuación se muestra una ejecución:

 

Arriba, vemos que las instrucciones:


    @ModelAttribute("p")
    public Personne getPersonne() {
        return new Personne(7,"abcd", 14);
}

han creado una entrada [p, Personne(7,'abcd',14)] en el modelo. Siempre es así.

Ahora consideremos el siguiente caso:


    // de lo contrario, se construye con el constructor por defecto
    // luego, los atributos del modelo se inicializan con los parámetros de GET o de POST

con la siguiente acción:


    // --------- el atributo de modelo [param1] forma parte del modelo, pero no está inicializado
    @RequestMapping(value = "/m22", method = RequestMethod.GET)
    public String m22(@ModelAttribute("param1") String p1, Model model) {
        return model.toString();
}
  • línea 3: el atributo de modelo de clave [param1] no existe. En este caso, el tipo asociado debe tener un constructor por defecto. Este es el caso aquí del tipo [String], pero no se puede escribir [@ModelAttribute("param1") Integer p1] porque la clase [Integer] no tiene un constructor por defecto;
  • línea 4: se devuelve el modelo para verificar si el atributo de modelo de clave [param1] forma parte de él;

A continuación se muestra un ejemplo de ejecución:

 

El atributo de modelo [param1] sí está presente en el modelo, pero el método [toString] del valor asociado no proporciona ninguna indicación sobre este valor.

Consideremos ahora la siguiente acción, en la que incluimos explícitamente una información en el modelo:


    // --------- el atributo de modelo [param2] se incluye explícitamente en el modelo
    @RequestMapping(value = "/m23", method = RequestMethod.GET)
    public String m23(String p2, Model model) {
        model.addAttribute("param2",p2);
        return model.toString();
}
  • línea 4: el valor [p2] recuperado en la línea 3 se inserta en el modelo asociado a la clave [param2]:

A continuación se muestra un ejemplo de ejecución:

 

Las reglas cambian si el parámetro de la acción es un objeto. A continuación se muestra un primer ejemplo:


    // ------ el atributo de modelo [unePersonne] se incluye automáticamente en el modelo
    @RequestMapping(value = "/m23b", method = RequestMethod.GET)
    public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
        return model.toString();
}

La acción no modifica la plantilla que se le ha proporcionado. El resultado es el siguiente:

Se observa que la anotación [@ModelAttribute("unePersonne") Personne p1] ha incluido a la persona [p1] en el modelo, asociada a la clave [unePersonne].

Consideremos ahora la siguiente acción:


    // --------- la persona p1 se incluye automáticamente en la plantilla
    // -------- con como clave el nombre de su clase, con el primer carácter en minúscula
    @RequestMapping(value = "/m23c", method = RequestMethod.GET)
    public String m23c(Personne p1, Model model) {
        return model.toString();
}
  • línea 4: no se ha incluido la anotación [@ModelAttribute];

El resultado es el siguiente:

Se observa que la presencia del parámetro [Personne p1] ha colocado a la persona [p1] en el modelo, asociada a la clave [personne], que es el nombre de la clase [Personne] con el primer carácter en minúscula.

4.18. [/m24]: validación del modelo de la acción

Consideremos el siguiente modelo de acción [ActionModel01]:

 

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // datos
    @NotNull
    private Integer a;
    @NotNull
    private Double b;

    // getters y setters
...
    }
  • líneas 8 y 9: la anotación [@NotNull] es una restricción de validación que indica que el dato anotado no puede tener el valor null;

Ahora analicemos la siguiente acción:


    // ----------------------- validación de un modelo ------------------------
    @RequestMapping(value = "/m24", method = RequestMethod.GET)
    public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // ¿Hay errores?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            // recorrido por la lista de errores
            for (FieldError error : result.getFieldErrors()) {
                buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
                        String.join(" - ", error.getCodes()), error.getCode(),error.getDefaultMessage()));
            }
            map.put("errors", buffer.toString());
        } else {
            // sin errores
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
}
  • línea 3: se instanciará un objeto [ActionModel01] y sus campos [a, b] se inicializarán con parámetros de los mismos nombres. La anotación [@Valid] indica que se deben verificar las restricciones de validez. Los resultados de esta verificación se colocarán en el parámetro de tipo [BindingResult] (segundo parámetro). Se llevarán a cabo las siguientes verificaciones:
    • debido a las anotaciones [@NotNull], deben estar presentes los parámetros [a] y [b];
    • debido al tipo [Integer a], el parámetro [a], que por naturaleza es de tipo [String], debe ser convertible a un tipo [Integer];
    • debido al tipo [Double b], el parámetro [b], que por naturaleza es de tipo [String], debe ser convertible a un tipo [Double];

Con la anotación [@Valid], los errores de validación se trasladarán al parámetro [BindingResult result]. Sin la anotación [@Valid], los errores de validación provocan que la acción falle y el servidor envía al cliente una respuesta HTTP con un estado 500 (Error interno del servidor).

  • línea 3: el resultado de la acción es de tipo [Map]. Se enviará al cliente la cadena jSON de este resultado. Se construyen dos tipos de diccionario:
    • en caso de falla, un diccionario con una entrada ['errors', value], donde [value] es una cadena de caracteres que describe todos los errores (línea 13);
    • en caso de éxito, un diccionario con una entrada ['data',value], donde [value] es a su vez un diccionario con dos entradas: ['a', value], ['b', value] (línea 19);
  • líneas 9-12: por cada error [error] detectado, se construye la cadena [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()]:
    • el primer elemento es el campo con error, [a] o [b],
    • el segundo elemento es el valor rechazado, por ejemplo, [x],
    • el tercer elemento es una lista de códigos de error. Veremos sus funciones más adelante;
    • el cuarto elemento es el código del error. Forma parte de la lista anterior;
    • el último elemento es el mensaje de error predeterminado. De hecho, puede haber varios mensajes de error;

A continuación, algunos ejemplos de ejecución:

En el ejemplo anterior, vemos que:

  • la asignación de 'x' al campo [ActionModel01.a] falló y el mensaje de error explica por qué;
  • la asignación de «y» al campo [ActionModel01.b] falló y el mensaje de error explica por qué;

Cabe destacar los códigos de error en el campo [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Volveremos a estos códigos de error cuando sea necesario personalizar el mensaje de error. Obsérvese que el código de error es [typeMismatch].

Otro ejemplo:

En este caso, no se pasaron los parámetros [a] y [b]. Los validadores [@NotNull] del modelo de acción [ActionModel01] cumplieron entonces su función;

Por fin, valores correctos:

4.19. [m/24]: personalización de los mensajes de error

Volvamos a una captura de pantalla del ejemplo anterior:

Arriba vemos los mensajes de error predeterminados. Es evidente que no podemos dejarlos así en una aplicación real. Es posible definir estos mensajes de error. Para ello, nos valeremos de los códigos de error. Arriba, vemos que el error para el campo [a] tiene los siguientes códigos: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. Estos códigos de error van de más preciso a menos preciso:

  • [typeMismatch.actionModel01.a]: error de tipo en el campo [a] del tipo [ActionModel01];
  • [typeMismatch.a]: error de tipo en un campo llamado [a];
  • [typeMismatch.java.lang.Integer]: error de tipo en un tipo Integer;
  • [typeMismatch]: error de tipo;

También se observa que el código de error del campo [a], obtenido a partir de [error.getCode()], es [typeMismatch] (véase la captura de pantalla anterior).

Vamos a colocar los mensajes de error en un archivo de propiedades:

  

El archivo [messages.properties] anterior quedará así:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.model01.a=Le paramètre [a] doit être entier

Cada línea tiene el siguiente formato:

    clé=message

Aquí, la clave será un código de error y el mensaje, el mensaje de error asociado a ese código.

Recordemos los códigos de error para los dos campos:

  • [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], cuando el parámetro [a] no es válido;
  • [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ] cuando el parámetro [b] no es válido;
  • [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull] cuando falta el parámetro [a];
  • [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] cuando falta el parámetro [b];

El archivo [messages.properties] debe incluir un mensaje de error para todos los casos de error posibles. En el caso de que:

  • los parámetros [a] y [b] faltan, se utilizará el código [NotNull];
  • si el parámetro [a] es incorrecto, hemos incluido mensajes para dos códigos: [typeMismatch.actionModel01.a, typeMismatch]. Veremos cuál se utiliza;
  • si el parámetro [b] es incorrecto, se utilizará el código [typeMismatch];

Para que se utilice el archivo [messages.properties], hay que configurar Spring:

  

Eliminamos las anotaciones de configuración de la clase [Application]:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}
  • línea 8: se inicia la aplicación Spring Boot. El primer parámetro del método estático [SpringApplication.run] es la clase que ahora configura la aplicación;

La clase [Config] es la siguiente:


package istia.st.springmvc.main;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
}
  • líneas 11-13: aquí se encuentran las anotaciones de configuración que antes estaban en la clase [Application];
  • línea 14: para configurar una aplicación Spring MVC, hay que extender la clase [WebMvcConfigurerAdapter];
  • línea 15: la anotación [@Bean] introduce un componente de Spring, un singleton;
  • línea 16: se define un bean llamado [messageSource] (el nombre del método). Este bean sirve para definir los archivos de mensajes de la aplicación y debe tener obligatoriamente este nombre;
  • líneas 17-19: le indican a Spring que el archivo de mensajes:
    • se encuentra en la carpeta [i18n] dentro del Classpath del proyecto (línea 18),
    • se llama [messages.properties] (línea 18). De hecho, el término [messages] es la raíz de los nombres de los archivos de mensajes, más que el nombre en sí mismo. Veremos que, en el contexto de la internacionalización, pueden existir varios archivos de mensajes, uno por cada cultura administrada. Así, podemos tener [messages_fr.properties] para el francés y [messages_en.properties] para el inglés. Los sufijos que se agregan a la raíz [messages] están estandarizados. No se puede poner cualquier cosa;

En el proyecto STS, hay que colocar la carpeta [i18n] en la carpeta de recursos, ya que esta se incluye en la ruta de clases (Classpath) del proyecto:

  

Para utilizar este archivo, creamos la siguiente acción nueva:


// Validación de un modelo, manejo de mensajes de error ------------------------
    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
        // el diccionario de resultados
        Map<String, Object> map = new HashMap<String, Object>();
        // el contexto de la aplicación Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // configuración regional
        Locale locale = RequestContextUtils.getLocale(request);
        // ¿Hay errores?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (FieldError error : result.getFieldErrors()) {
                // búsqueda del mensaje de error a partir de los códigos de error
                // se busca el mensaje en los archivos de mensajes
                // los códigos de error en forma de tabla
                String[] codes = error.getCodes();
                // en forma de cadena
                String listCodes = String.join(" - ", codes);
                // ¿Se ha encontrado
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // ¿Se encontró?
                if (msg == null) {
                    throw new Exception(String.format("Indiquez un message pour l'un des codes [%s]", listCodes));
                }
                // se encontró: se agrega el mensaje de error a la lista de mensajes de error
                buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
                        String.join(" - ", msg)));
            }
            map.put("errors", buffer.toString());
        } else {
            // ok
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
    }

Este código es similar al de la acción [/m24]. A continuación explicamos las diferencias:

  • línea 3: inyectamos la solicitud [HttpServletRequest request] en los parámetros de la acción. La vamos a necesitar;
  • líneas 7-8: recuperamos el contexto de Spring. Este contexto contiene todos los beans de Spring de la aplicación. También permite acceder a los archivos de mensajes;
  • línea 10: recuperamos la configuración regional de la aplicación. Este término se explica con más detalle más adelante;
  • líneas 15-31: para cada error, buscamos un mensaje que corresponda a uno de estos códigos de error. Se buscan en el orden de los códigos que aparecen en [error.getCodes()]. En cuanto se encuentra un mensaje, nos detenemos;
  • línea 26: cómo recuperar un mensaje en [messages.properties]:
    • el primer parámetro es el código que se busca en [messages.properties],
    • el segundo es una matriz de parámetros, ya que a veces los mensajes tienen parámetros. Este no es el caso aquí,
    • el tercero es la configuración regional utilizada (obtenida en la línea 10). La configuración regional indica el idioma utilizado: [fr_FR] para el francés de Francia, [en_US] para el inglés de USA. El mensaje se busca en messages_[locale].properties; por ejemplo, en [messages_fr_FR.properties]. Si este archivo no existe, el mensaje se busca en [messages_fr.properties]. Si ese archivo no existe, se busca el mensaje en [messages.properties]. Este último caso es el que nos servirá;
  • líneas 25-29: de manera un poco inesperada, cuando se busca un código inexistente en un archivo de mensajes, se produce una excepción en lugar de un puntero nulo;
  • líneas 33-35: se maneja el caso en que no hay mensaje de error;
  • líneas 37-38: se construye la cadena de error. En ella, se incluyen la configuración regional y el mensaje de error encontrado;

A continuación se muestran algunos ejemplos de ejecución:

 

Se observa que:

  • la configuración regional de la aplicación es [fr_FR]. Es un valor predeterminado, ya que no hemos hecho nada para inicializarla;
  • que el mensaje utilizado para ambos campos es el siguiente:

NotNull=Le champ ne peut être vide

Otro ejemplo:

 

Se observa que:

  • el mensaje de error utilizado para el parámetro [a] es el siguiente:

typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
  • el mensaje de error utilizado para el parámetro [b] es el siguiente:

typeMismatch=Format invalide

¿Por qué hay dos mensajes diferentes? Para el parámetro [a], había dos mensajes posibles:


typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

Los códigos de error se analizaron en el orden de la tabla [error.getCodes()]. Resulta que este orden va del código más específico al más general. Por eso se encontró primero el código [typeMismatch.model01.a].

4.20. [/m25]: internacionalización de una aplicación Spring MVC

Ahora que sabemos cómo personalizar los mensajes de error en francés, nos gustaría tenerlos también en inglés, lo que nos lleva a la internacionalización de una aplicación Spring MVC. Para gestionarla, vamos a ampliar la clase de configuración [Config], que queda así:


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}
  • líneas 28-32: creamos un interceptor de solicitud. Un interceptor de solicitud extiende la interfaz [HandlerInterceptor]. Esta clase inspecciona la solicitud entrante antes de que sea procesada por una acción. Aquí, el interceptor [localeChangeInterceptor] buscará un parámetro llamado [lang] en la solicitud entrante, GET o POST, y modificará la configuración regional de la aplicación según ese parámetro. Así, si el parámetro es [lang=en_US], la configuración regional de la aplicación pasará a ser el inglés de USA;
  • líneas 34-37: se redefine el método [WebMvcConfigurerAdapter.addInterceptors] para agregar el interceptor anterior;
  • líneas 39-45: sirven para configurar la forma en que la configuración regional se encapsulará en una cookie. Sabemos que una cookie puede servir como memoria del usuario, ya que el navegador del cliente la reenvía sistemáticamente al servidor. El interceptor [localeChangeInterceptor] anterior crea una cookie que encapsula la configuración regional. La línea 42 le asigna el nombre [lang] a esta cookie. La cookie también se utiliza para cambiar la configuración regional;
  • línea 43: indica que, en ausencia de la cookie [lang], la configuración regional será [fr];

En resumen, la configuración regional de una solicitud se puede establecer de dos maneras:

  • pasando un parámetro llamado [lang];
  • enviando una cookie llamada [lang]. Esta cookie se crea automáticamente al finalizar el método anterior;

Para aprovechar esta configuración regional, crearemos archivos de mensajes para las configuraciones regionales [fr] y [en]:

 

El archivo [messages_fr.properties] es el siguiente:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier

El archivo [messages_en.properties] es el siguiente:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer

El archivo [messages.properties] es una copia del archivo [messages_en.properties]. Cabe recordar que el archivo [messages.properties] se utiliza cuando no se encuentra ningún archivo que corresponda a la configuración regional de la solicitud. En nuestro caso, si el usuario envía un parámetro [lang=en], dado que el archivo [messages_en.properties] no existe, se utilizará el archivo [messages.properties]. Por lo tanto, el usuario verá mensajes en inglés.

Probemos. En primer lugar, en el entorno de desarrollo de Chrome (Ctrl-Mayús-I), revisa tus cookies:

 

Si tienes una cookie llamada [lang], elimínala. Luego, con Chrome, solicita URL y [http://localhost:8080/m25]:

 

El navegador envió los siguientes encabezados HTTP:

GET /m25 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36
Referer: http://localhost:8080/m25
Accept-Encoding: gzip, deflate, sdch
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4

Se observa que en estos encabezados no hay ninguna cookie [lang]. En este caso, nuestro código utiliza la configuración regional [fr]. Así lo muestra la captura de pantalla. Probemos con otro caso:

  • en [1], hemos pasado el parámetro [lang=en] para cambiar la configuración regional a [en];
  • en [2], se ve la nueva configuración regional;
  • en [3], el mensaje se muestra en inglés;

Veamos ahora los intercambios en HTTP:

 

Como se ve arriba, el servidor devolvió una cookie [lang]. Esto tiene una consecuencia importante: la configuración regional de la próxima solicitud volverá a ser [en] debido a la cookie [lang] que el navegador va a enviar. Por lo tanto, deberíamos mantener los mensajes en inglés. Verifiquémoslo:

 

En el ejemplo anterior, vemos que la configuración regional se ha mantenido en [en]. Debido a la cookie que envía sistemáticamente el navegador, se mantendrá así mientras el usuario no la cambie enviando el parámetro [lang] de la siguiente manera:

 

4.21. [/m26]: inserción de la configuración regional en la plantilla de la acción

En el ejemplo anterior, vimos una forma de recuperar la configuración regional de la solicitud:


    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
...
        // local
        Locale locale = RequestContextUtils.getLocale(request);
// ¿Hay errores?

La configuración regional se puede inyectar directamente en los parámetros de la acción. A continuación, un ejemplo:


    @RequestMapping(value = "/m26", method = RequestMethod.GET)
    public String m26(Locale locale) {
        return String.format("locale=%s", locale.toString());
}
 

Como se ve arriba, no se verifica la validez de la configuración regional solicitada. Sin embargo, la siguiente solicitud del navegador provoca una excepción del lado del servidor porque la cookie de configuración regional que recibe es incorrecta.

4.22. [/m27]: verificar la validez de un modelo con Hibernate Validator

Consideremos la siguiente acción nueva:


    //validación de un modelo con Hibernate Validator ------------------------
    @RequestMapping(value = "/m27", method = RequestMethod.POST)
    public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // ¿Hay errores?
        if (result.hasErrors()) {
            // recorrido por la lista de errores
            for (FieldError error : result.getFieldErrors()) {
                map.put(error.getField(),
                        String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
            }
        } else {
            // sin errores
            map.put("data", data);
        }
        return map;
}

Aquí tenemos un código que ya hemos visto varias veces:

  • línea 3: la acción [/m27] se solicita a través de un POST;
  • líneas 8-11: cada error se identificará mediante [champ, message] con:
    • campo: el campo con error,
    • mensaje: el mensaje de error asociado, así como la lista de códigos de error;
  • línea 14: si no hay errores, se devuelve la cadena jSON con los valores enviados;

En la línea 3, se utiliza el modelo de acción [ActionModel02] siguiente:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel02 {

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
    private Boolean assertFalse;
    
    @NotNull(message = "La donnée est obligatoire")
    @AssertTrue(message = "Seule la valeur [true] est acceptée")
    private Boolean assertTrue;
    
    @NotNull(message = "La donnée est obligatoire")
    @Future(message = "Il faut une date postérieure à aujourd'hui")
    private Date dateInFuture;
    
    @NotNull(message = "La donnée est obligatoire")
    @Past(message = "Il faut une date antérieure à aujourd'hui")
    private Date dateInPast;
    
    @NotNull(message = "La donnée est obligatoire")
    @Max(value = 100, message = "Maximum 100")
    private Integer intMax100;
    
    @NotNull(message = "La donnée est obligatoire")
    @Min(value = 10, message = "Minimum 10")
    private Integer intMin10;
    
    @NotNull(message = "La donnée est obligatoire")
    @NotBlank(message = "La chaîne doit être non blanche")
    private String strNotBlank;
    
    @NotNull(message = "La donnée est obligatoire")
    @Size(min = 4, max = 6, message = "La chaîne doit avoir entre 4 et 6 caractères")
    private String strBetween4and6;
    
    @NotNull(message = "La donnée est obligatoire")
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "Le format doit être hh:mm:ss")
    private String hhmmss;
    
    @NotNull(message = "La donnée est obligatoire")
    @Email(message = "Adresse invalide")
    private String email;
    
    @NotNull(message = "La donnée est obligatoire")
    @Length(max = 4, min = 4, message = "La chaîne doit avoir 4 caractères exactement")
    private String str4;
    
    @Range(min = 10, max = 14, message = "La valeur doit être dans l'intervalle [10,14]")
    @NotNull(message = "La donnée est obligatoire")
    private Integer int1014;
    
    @URL(message = "URL invalide")
    private String url;

    // métodos getter y setter

...
}

La clase utiliza restricciones de validación procedentes de dos paquetes:

  • [javax.validation.constraints] en las líneas 5-13;
  • [org.hibernate.validator.constraints] en las líneas 15 a 19;

Las dependencias de Maven de estos dos paquetes están presentes en el proyecto:

  

Aquí no vamos a utilizar mensajes internacionalizados, sino mensajes definidos dentro de la restricción con el atributo [message]. Para probar esta acción, utilizaremos [Advanced Rest Client]:

  • en [1-2], la consulta POST;
  • en [3], el encabezado HTTP [Content-Type] que se debe utilizar;
  • en [4], el enlace [Add new value] permite agregar un par [paramètre, value];
  • en [5], colocar un campo de [ActionModel02], en este caso el campo [assertFalse]:

    @NotNull(message = "La donnée est obligatoire")
    @AssertFalse(message = "Seule la valeur [false] est acceptée")
private Boolean assertFalse;
  • en [6], ingresa un valor incorrecto para ver un mensaje de error. Arriba, la restricción [@AssertFalse] exige que el campo [assertFalse] tenga el valor [false];
  • en [7], la respuesta del servidor: se activó la restricción [@NotNull] por campos vacíos y se mostró el mensaje de error asociado;
  • en [8], el mensaje del campo [assertFalse] para el cual no se cumplió la restricción [@AssertFalse], así como los códigos de este error. Cabe recordar que estos códigos pueden estar asociados a mensajes internacionalizados;

He aquí otro ejemplo:

 

Image

Se invita al lector a probar los diferentes casos de error hasta llegar a POST, con datos todos válidos:

Nota: el formato de las fechas es el formato anglosajón: mm/dd/aaaa.

4.23. [/m28]: externalización de los mensajes de error

En la clase [ActionModel02], hemos incluido los mensajes de forma «estática». Es preferible externalizarlos en archivos de mensajes. Seguimos el ejemplo de la acción [/m25]. Creamos la siguiente nueva plantilla de acción [ActionModel03]:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel03 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;
    
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    private Date dateInFuture;
    
    @NotNull
    @Past
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    private String url;

    // getters y setters
        ...
}

Los mensajes de error se externalizan en los archivos [messages.properties]:

  

El archivo [messages_fr.properties] es el siguiente:


NotNull=Le champ ne peut être vide
typeMismatch=Format invalide
typeMismatch.actionModel01.a=Le paramètre [a] doit être entier
Range.actionModel03.int1014=La valeur doit être dans l'intervalle [10,14]
NotBlank.actionModel03.strNotBlank=La chaîne doit être non blanche
AssertFalse.actionModel03.assertFalse=Seule la valeur [false] est acceptée
Pattern.actionModel03.hhmmss=Le format doit être hh:mm:ss
Past.actionModel03.dateInPast=Il faut une date antérieure ou égale à celle d'aujourd'hui
Future.actionModel03.dateInFuture=Il faut une date postérieure à celle d'aujourd'hui
Length.actionModel03.str4=La chaîne doit avoir 4 caractères exactement
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Seule la valeur [true] est acceptée
Email.actionModel03.email=Adresse invalide
Size.actionModel03.strBetween4and6=La chaîne doit avoir entre 4 et 6 caractères
URL.actionModel03.url=URL invalide

Los mensajes de error se han agregado a las líneas 4 a 16. Tienen el siguiente formato:

code=message

Los códigos no pueden ser cualquiera. Son los que se muestran en la acción anterior [/m27]. Por ejemplo:

Image

En los archivos de mensajes, para el campo [int1014] se debe utilizar uno de los cuatro códigos anteriores.

El archivo [messages_en.properties] es el siguiente:


NotNull=The field can't be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in [10,14] interval
NotBlank.actionModel03.strNotBlank=String can't be empty
AssertFalse.actionModel03.assertFalse=Only boolean [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be before or equal to today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL

La plantilla de acción [ActionModel03] es utilizada por la siguiente acción:


// ----------------------- externalización de los mensajes de error ------------------------
    @RequestMapping(value = "/m28", method = RequestMethod.POST)
    public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        // el contexto de la aplicación Spring
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // configuración regional
        Locale locale = RequestContextUtils.getLocale(request);
        // ¿de los errores?
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                // búsqueda del mensaje de error a partir de los códigos de error
                // el mensaje se busca en los archivos de mensajes
                // los códigos de error en forma de tabla
                String[] codes = error.getCodes();
                // en forma de cadena
                String listCodes = String.join(" - ", codes);
                // ¿Se ha encontrado la búsqueda
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // ¿Se encontró algo?
                if (msg == null) {
                    msg = String.format("Indiquez un message pour l'un des codes [%s]", listCodes);
                }
                // se encontró: se agrega el error al diccionario
                map.put(error.getField(), msg);
            }
        } else {
            // sin errores
            map.put("data", data);
        }
        return map;
    }

Ya hemos comentado este tipo de código. Lo único realmente importante es la línea 23: el mensaje de error que se muestra depende de la configuración regional de la solicitud.

Aquí hay un ejemplo en francés:

y ahora en inglés: