Skip to content

3. Acciones: la respuesta

Consideremos la arquitectura de una aplicación Spring MVC:

En este capítulo, analizamos el proceso que lleva la solicitud [1] al controlador y a la acción [2a] que la procesarán, un mecanismo que se denomina enrutamiento. Además, presentamos las diferentes respuestas [3] que una acción puede enviar al navegador. Puede tratarse de algo distinto a una vista V [4b].

3.1. El nuevo proyecto

Creamos un nuevo proyecto Spring:

  • en [1-2], creamos un nuevo proyecto basado en Spring Boot;
  • en [3], el nombre del proyecto Maven;
  • en [4], el grupo Maven en el que se colocará el resultado de la compilación del proyecto;
  • en [5], el nombre dado al producto de la compilación;
  • en [6], una descripción del proyecto;
  • en [7], el paquete en el que se colocará la clase ejecutable del proyecto;
  • en [8], la naturaleza del proyecto. Se trata de un proyecto web con vistas Thymeleaf. Aquí se pueden ver todas las dependencias de Maven listas para usar que ofrece el proyecto Spring Boot;
  • en [9], se indica que el producto resultante de la compilación de Maven se empaquetará en un archivo jar y no en un archivo WAR. El proyecto utilizará entonces un servidor Tomcat integrado que se encontrará entre sus dependencias;
  • en [10], se continúa con el asistente;
  • en [11], se indica la carpeta del proyecto;
  • en [12], el proyecto generado;
  • en [14-15], se renombra el paquete [istia.st.springmvc];
  • en [16], el nuevo nombre del paquete;
  • en [17], el nuevo proyecto;

Ahora creamos una nueva clase;

  • en [1-3], creamos una nueva clase;
  • en [5] le asignamos y en [4] especificamos su paquete;
  • en [6] el nuevo proyecto;

La clase es, por el momento, la siguiente:


package istia.st.springmvc;

public class ActionsController {

}

Modificamos este código de la siguiente manera:


package istia.st.springmvc;

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

@RestController
public class ActionsController {

}
  • línea 6: la anotación [@RestController] indica dos cosas:
    • que la clase [ActionsController] así anotada es un controlador Spring MVC, por lo que contiene acciones que procesan URL de clients;
    • que el resultado de estas acciones se envía al cliente;

La otra anotación [@Controller] que hemos encontrado es diferente: las acciones de un controlador así anotado devuelven el nombre de la vista que debe mostrarse. Es entonces la combinación de esta vista y del modelo construido por la acción para esta vista la que proporciona la respuesta enviada al cliente.

El cambio de estructura de nuestro proyecto conlleva un cambio en la configuración del mismo:

  

La clase [Application] evoluciona de la siguiente manera:


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: la anotación [ComponentScan] admite como parámetro una matriz de nombres de paquetes donde Spring Boot debe buscar componentes Spring. Aquí incluimos en esta matriz el paquete [istia.st.springmvc.controllers] para que se encuentre el controlador anotado con [@RestController];

Vamos a crear varias acciones en el controlador para ilustrar sus principales características. En primer lugar, nos centraremos en los distintos tipos de respuestas posibles de una acción en una aplicación sin vistas.

3.2. [/a01, /a02] - Hello world

Nuestra primera acción será la siguiente:


@RestController
public class ActionsController {
    // ----------------------- hola mundo ------------------------
    @RequestMapping(value = "/a01", method = RequestMethod.GET)
    public String a01() {
        return "Greetings from Spring Boot!";
    }
}
  • línea 4: la anotación [RequestMapping] califica la solicitud procesada por la acción anotada:
    • El atributo [value] es el URL procesado,
    • el atributo [method] establece el método aceptado;

Así, el método [a01] procesa la solicitud HTTP [GET /a01].

  • línea 5: el método [a01] devuelve un tipo [String] que se enviará tal cual al cliente;
  • línea 6: la cadena devuelta;

Ejecutemos la aplicación como ya lo hemos hecho varias veces y, a continuación, con el cliente [Advanced Rest Client], solicitamos el URL [/a01] con un GET [1-2]:

  • en [3], la respuesta del servidor;
  • en [4], los encabezados HTTP de la respuesta. Se observa que la codificación utilizada es [ISO-8859-1]. Se puede preferir la codificación UTF-8. Esto se puede configurar;
  • en [5], solicitamos el mismo URL con el navegador Chrome;

Añadimos la siguiente acción [/a02] en el controlador [ActionsController] (por lo que a veces se confundirá URL con el método que la procesa bajo el nombre de acción):


    // ----------------------- caracteres acentuados - UTF8 ------------------------
    @RequestMapping(value = "/a02", method = RequestMethod.GET, produces="text/plain;charset=UTF-8")
    public String a02() {
        return "caractères accentués : éèàôûî";
}
  • línea 2: el atributo [produces="text/plain;charset=UTF-8"] indica que la acción envía un flujo de texto con caracteres codificados en el formato [UTF-8]. Este formato permite, en particular, el uso de caracteres acentuados;

Para que se tenga en cuenta esta nueva acción, debemos reiniciar la aplicación:

 

El resultado es el siguiente:

  • en [1], vemos la naturaleza del documento enviado por el servidor;
  • en [2-3], se ven correctamente los caracteres acentuados;

3.3. [/a03]: generar un flujo XML

Añadimos la siguiente acción [/a03]:


    // ----------------------- text/xml ------------------------
    @RequestMapping(value = "/a03", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
    public String a03() {
        String greeting = "<greetings><greeting>Greetings from Spring Boot!</greeting></greetings>";
        return greeting;
}
  • línea 2: el atributo [produces="text/xml;charset=UTF-8"] indica que la acción envía un flujo XML con caracteres codificados en el formato [UTF-8];

Su ejecución da como resultado lo siguiente:

  • en [1], el encabezado HTTP especifica que el documento enviado es HTML;
  • en [2], el navegador Chrome utiliza esta información para dar formato al texto XML recibido;

Recordemos que, con Chrome, se puede acceder a los intercambios HTTP entre el cliente y el servidor en la ventana de desarrollo (Ctrl-Mayús-I):

Image

A partir de ahora, no se realizarán sistemáticamente capturas de pantalla de los intercambios HTTP entre el cliente y el servidor. En ocasiones, nos limitaremos a indicar el texto de dichos intercambios.

3.4. [/a04, /a05]: generar un flujo jSON

Añadimos la siguiente acción [/a04]:


    // ----------------------- generar jSON ------------------------
    @RequestMapping(value = "/a04", method = RequestMethod.GET)
    public Map<String, Object> a04() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("1", "un");
        map.put("2", new int[] { 4, 5 });
        return map;
}
  • línea 3: la acción devuelve un tipo [Map], un diccionario. Recordemos que, con un controlador de tipo [@RestController], el resultado de la acción es la respuesta enviada al cliente. Dado que el protocolo HTTP es un protocolo de intercambio de líneas de texto, la respuesta del cliente debe serializarse en una cadena de caracteres. Para ello, Spring MVC utiliza diversos convertidores [Objet <---> chaîne de caractères]. La asociación de un objeto concreto con un convertidor se realiza mediante configuración. En este caso, la autoconfiguración de Spring Boot inspeccionará las dependencias del proyecto:
 

Las dependencias de Jackson mencionadas anteriormente son bibliotecas de serialización/deserialización de objetos en cadenas jSON. Spring Boot utilizará entonces estas bibliotecas para serializar/deserializar los objetos devueltos por las acciones. En el apartado 9.7 se incluye un ejemplo de código Java para serializar y deserializar objetos Java en jSON.

Cabe señalar en la línea 2 que no hemos especificado el tipo de la respuesta enviada. Veremos el tipo por defecto que se enviará.

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

Añadamos ahora la siguiente acción [/a05]:


    // ----------------------- generar jSON - 2 ------------------------
    @RequestMapping(value = "/a05", method = RequestMethod.GET)
    public Personne a05() {
        return new Personne(1,"carole",45);
}

La clase [Personne] es la siguiente:

  

package istia.st.sprinmvc.models;

public class Personne {

    // identificador
    private Integer id;
    // nombre
    private String nom;
    // edad
    private int age;

    // constructores
    public Personne() {

    }

    public Personne(String nom, int age) {
        this.nom = nom;
        this.age = age;
    }

    public Personne(Integer id, String nom, int age) {
        this(nom, age);
        this.id = id;
    }

    @Override
    public String toString() {
        return String.format("[id=%s, nom=%s,  age=%d]", id, nom, age);
    }

    // getters y setters
...
}

La ejecución da los siguientes resultados:

  • en [1], el servidor indica que el documento que envía es jSON;
  • en [2], el documento jSON recibido;

3.5. [/a06]: devolver un flujo vacío

Añadimos la siguiente acción [/a06]:


    // ----------------------- devolver un flujo vacío ------------------------
    @RequestMapping(value = "/a06")
    public void a06() {
}
  • línea 3, la acción [/a06] no devuelve nada. Spring MVC generará entonces una respuesta vacía al cliente;

La ejecución da los siguientes resultados:

 

En el ejemplo anterior, el atributo HTTP [Content-Length] de la respuesta indica que el servidor envía un documento vacío.

3.6. [/a07, /a08, /a09]: naturaleza del flujo con [Content-Type]

Añadimos la siguiente acción [/a07]:


    // ----------------------- text/html ------------------------
    @RequestMapping(value = "/a07", method = RequestMethod.GET, produces = "text/html;charset=UTF-8")
    public String a07() {
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        return greeting;
}
  • línea 2, la acción [/a07] devuelve un flujo HTML [text/html];
  • línea 4: una cadena HTML;

La ejecución da los siguientes resultados:

  • en [1], vemos que Chrome ha interpretado la etiqueta HTML <h1>, que muestra su contenido en caracteres grandes;

Ahora hagamos lo mismo con la siguiente acción [/a08]:


    // ----------------------- resultado HTML en text/plain ------------------------
    @RequestMapping(value = "/a08", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String a08() {
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        return greeting;
}
  • línea 2: la respuesta de la acción es de tipo [text/plain];

Los resultados son los siguientes:

  • en [1], Chrome no interpretó la etiqueta HTML <h1> porque el servidor le indicó que le enviaba un flujo [text/plain] [2];

Repitamos algo similar con la siguiente acción [/a09]:


    // ----------------------- resultado HTML en text/xml ------------------------
    @RequestMapping(value = "/a09", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
    public String a09() {
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        return greeting;
}
  • línea 2: se envía un flujo de tipo [text/xml];

Los resultados son los siguientes:

  • en [1], Chrome no interpretó la etiqueta HTML <h1> porque el servidor le indicó que le enviaba un flujo [text/xml] [2]. Por lo tanto, gestionó la etiqueta <h1> como una etiqueta XML;

De estos ejemplos se desprende la importancia del encabezado HTTP [Content-Type] en la respuesta del servidor. El navegador utiliza este encabezado para saber cómo interpretar el documento que recibe;

3.7. [/a10, /a11, /a12]: redirigir al cliente

Creamos un nuevo controlador [RedirectController]:

 

El código de [RedirectCntroller] será, por el momento, el siguiente:


package istia.st.springmvc.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class RedirectController {
}
  • línea 7: utilizamos la anotación [@Controller], lo que hace que, a partir de ahora, por defecto, el tipo [String] del resultado de las acciones designe el nombre de una acción o de una vista;

Creamos la siguiente acción [/a10]:


    // ------------ redireccionamiento a una acción de terceros -----------------------
    @RequestMapping(value = "/a10", method = RequestMethod.GET)
    public String a10() {
        return "a01";
}
  • línea 4: devolvemos como resultado «a01», que es el nombre de una acción. Será ella quien envíe la respuesta al cliente;

He aquí un ejemplo:

  • en [2], hemos recibido el flujo de la acción [/a01];
  • en [3], el navegador muestra el URL de la acción [/a10];

Ahora creamos la siguiente acción [/a11]:


    // ------------ redirección temporal 302 a una acción de terceros -----------------------
    @RequestMapping(value = "/a11", method = RequestMethod.GET)
    public String a11() {
        return "redirect:/a01";
}

Obtenemos los siguientes resultados:

  • en los registros de Chrome [1-2], vemos dos solicitudes, una hacia [/a11] y otra hacia [/a01];
  • en [3], el servidor responde con un código [302] que solicita al navegador cliente que se redirija a laURL indicado por el encabezado HTTP [Location:] [4]. El código [302] es un código de redireccionamiento temporal;

A continuación, el navegador realiza la segunda solicitud a la redirección URL:

  • en [5], la segunda solicitud del cliente;
  • en [6], el navegador del cliente muestra el URL de la solicitud de redirección;

Es posible que se desee indicar una redirección permanente, en cuyo caso hay que enviar al cliente el siguiente encabezado HTTP:

HTTP/1.1 301 Moved Permanently 

lo que significa que la redirección es permanente. Algunos motores de búsqueda tienen en cuenta esta diferencia entre redirección temporal (302) y permanente (301).

Escribimos la acción [/a12] que llevará a cabo esta redirección permanente:


    // ------------ redirección permanente 301 a una acción de terceros----------------
    @RequestMapping(value = "/a12", method = RequestMethod.GET)
    public void a12(HttpServletResponse response) {
        response.setStatus(301);
        response.addHeader("Location", "/a01");
}
  • línea 3: se le pide a Spring MVC que inyecte el objeto [HttpServletResponse], que encapsula la respuesta enviada al cliente;
  • línea 4: se establece el [status] de la respuesta, el [301] del encabezado HTTP:
HTTP/1.1 301 Moved Permanently
  • línea 5: se crea manualmente el encabezado HTTP siguiente:
Location: /a01 

que es el URL de redireccionamiento.

La ejecución da los siguientes resultados:

De este ejemplo se desprende cómo:

  • generar el estado de la respuesta HTTP;
  • incluir un encabezado HTTP en la respuesta;

3.8. [/a13]: generar la respuesta completa

Es posible controlar totalmente la respuesta, como muestra la siguiente acción de la clase [ResponsesController]:

  

    // ----------------------- generación completa de la respuesta ------------------------
    @RequestMapping(value = "/a13")
    public void a13(HttpServletResponse response) throws IOException {
        response.setStatus(666);
        response.addHeader("header1", "qq chose");
        response.addHeader("Content-Type", "text/html;charset=UTF-8");
        String greeting = "<h1>Greetings from Spring Boot!</h1>";
        response.getWriter().write(greeting);
}
  • línea 3: el resultado de la acción es [void]. En este caso, para enviar una respuesta no vacía al cliente, hay que utilizar el objeto [HttpServletResponse response] proporcionado por Spring MVC;
  • línea 4: se le da a la respuesta un estado que no será reconocido por el cliente;
  • línea 5: se añade un encabezado HTTP que no será reconocido por el cliente;
  • línea 6: se añade un encabezado HTTP [Content-Type] para especificar el tipo de flujo que se va a enviar, en este caso HTML;
  • líneas 7-8: el documento que seguirá a los encabezados HTTP en la respuesta;

Los resultados son los siguientes:

  • en [1], reconocemos los elementos de nuestra respuesta;
  • en [2-3], vemos que Chrome ha ignorado el hecho de que:
    • el estado HTTP de la respuesta no era un estado HTTP reconocido,
    • que el encabezado [header1] no era un encabezado HTTP reconocido;

Si el cliente no es un navegador sino un cliente programado, se pueden utilizar los estados y los encabezados que se desee.