Spring MVC permite crear fácilmente
una aplicación web en un contenedor ligero como Tomcat
o Jetty. Permite servir una interfaz hombre-máquina (IHM)
con JSP o JSF u otro motor de renderizado como Thymeleaf y exponer API
SOAP y REST. Es muy fácil de utilizar y permite tener una
muy buena cobertura de pruebas. Es posible usar una configuración
basada en archivos XML o anotaciones. Al igual que sucede con otros
tipos de aplicaciones Spring, son preferibles las configuraciones
anotadas porque son más fáciles de mantener. En
este capítulo, presentamos los dos tipos de configuración
porque los proyectos más antiguos todavía usan
la versión basada en archivos XML. Los servicios web SOAP
y REST algunas veces usan Spring MVC, junto con Jersey y Apache
CXF.
Este capítulo se centra en Spring
MVC sin Spring Boot, que será el tema de un capítulo
específico (Spring Boot). Para los nuevos proyectos, normalmente
ya no se utilizan las JSP. En teoría, las páginas
JSP se han sustituido por páginas JSF, pero en la práctica
las SPA (Single Page Applications)
han tenido prioridad sobre las JSP (y JSF). Sin embargo, todavía
se utilizan las partes inicialmente previstas para un uso JSF con
el RestTemplate, lo que permite el uso simplificado de Spring MVC
para su utilización con servidores REST. Sin embargo, sigue
siendo interesante conocer esta parte para poder hacer mantenimiento y
pasar ciertas pruebas técnicas durante la selección.
Preferiblemente, usaremos el WebClient del
stack reactivo para los nuevos proyectos que hagan Spring MVC (bloqueadores)
porque RestTemplate ya no evolucionará a favor de evoluciones
en el WebClient. De hecho, el WebClient es un cliente de bloqueo
normal y un cliente reactivo. El WebClient se detalla en el capítulo
titulado Introducción a Spring Reactor y Spring Webflux.
Al igual que sucede con muchos módulos
de Spring, encontramos en los proyectos de configuraciones mezcladas
varios estilos correspondientes a varios períodos
de programación. Este capítulo muestra maneras
antiguas y nuevas de usar Spring MVC. Cuanto más moderna
es la configuración, más sencillo parece. Por
ejemplo, todavía es posible configurar una aplicación Spring
MVC de manera muy fina usando programación, pero entender
el origen de los argumentos de configuración en formato
XML permite navegar más fácilmente a través
de una comprensión transversal.
La configuración se debe documentar
con comentarios en los archivos. De hecho, es más fácil
hacerlo siempre y cuando el tema sea nuevo. La configuración
concentra muchas dificultades durante sus evoluciones. También
veremos en un capítulo específico que es posible
hacer aplicaciones reactivas Spring WebFlux y que, en este caso,
gran parte de la complejidad de la configuración está oculta,
pero permanece accesible. Como de costumbre, la mejor configuración
sigue siendo la más sencilla con respecto a la necesidad
que se debe cubrir.
Los ejemplos de este capítulo son
muy detallados. No se ha podido transferir todo el código
a estas páginas. Lo ideal es tener en su IDE los ejemplos
del libro para ver cómo se utilizan los elementos y, si
es necesario, poder hacer algunas pruebas durante la lectura para
retener los muchísimos elementos que se presentan. Los
nombres de los elementos relativos a los ejemplos se suelen indicar en
el capítulo correspondiente.
Esta sección muestra el uso de la
parte de servidor, con un foco sobre el uso de servlets controlados
por Spring en un contexto MVC, con la parte front en JSP. En las
aplicaciones más modernas usamos JSF, y en aplicaciones
muy modernas solo usamos las API REST o GraphQL con la
parte front hecha con frameworks como Angular, como veremos en el
capítulo sobre el uso avanzado de este framework.
Sin embargo, durante nuestras misiones, será normal
que tengamos que trabajar con aplicaciones antiguas. Históricamente,
disponíamos de las aplicaciones Struts 1, luego
Struts 2 o Spring MVC, que era la solución competidora.
2. Elementos complejos del controlador Controlador
La clase controlador anotada con @Controllergestiona el enrutamiento
de las consultas.
a. Funcionamiento global del controlador
El controlador interpreta los datos que provienen
del cliente y los transforma en un modelo correspondiente a una
vista para el usuario.
Utilizamos la anotación @Controller para identificar
los controladores.
El Request Mapping se puede hacer desde los
métodos o desde la clase para un primer nivel, seguido
por el método para un segundo nivel.
Un primer nivel en la clase y un segundo en
el método:
@Controller
@RequestMapping("/class-mapping/*")
public class ClasslevelMappingController {
@RequestMapping("/pathc")
public @ResponseBody String porElPath() {
return "Mapeado por el path";
}
es equivalente a un nivel únicamente
en el método:
@Controller
public class PorMappingController {
@RequestMapping("/mapping/pathm")
public @ResponseBody String porElPath() {
return "Mapeado por el path";
}
El controlador más simple indica
para una URL /mapping/pathm que
el método porElPath() realiza
el procesamiento y devuelve la página.
Para abreviar, en los ejemplos nos basamos
en un enrutamiento solo en el método utilizado
por los controladores, pero el principio sigue siendo el mismo si
compartimos el enrutamiento entre la clase y el método.
La prueba unitaria por path es un poco más
compleja porque tiene que crear un mock MockMvc e
inicializarlo:
@RunWith(SpringJUnit4ClassRunner.class)
public class ParMappingControllerTests extends
AbstractContextControllerTests {
private MockMvc mockMvc;
@Before
public void setup() throws Exception {
this.mockMvc =
webAppContextSetup(this.wac).alwaysExpect(status().isOk())
.build();
@Test
public void byPathc() throws Exception {
this.mockMvc.perform(get("/mapping/pathc")).andExpect(content()
.string("Mapeado por el path"));
Reutilizamos el mock mockMvc para
el resto de las pruebas del Request Mapping.
b. Ejemplo de clase simple
Comenzaremos con el caso más simple.
Se trata de mostrar una página que responde a una consulta
GET sobre la URL /primera:
Código en la página:
Añadimos un href para
apuntar al servlet:
<a id="simpleLink" class="textLink" href="<c:url value="/primera"
/>">GET /primera</a>
En el controlador:
Este controlador es sencillo:
@RequestMapping("/primera")
public @ResponseBody String simple() {
logger.debug("Respuesta");
return "ENI os dice hola";
}
Pruebas unitarias:
La TU carga la página, comprueba
el estado y el tipo del contenido.
@Test
public void simple() throws Exception {
standaloneSetup(new PrimerControlador()).build()
.perform(get("/primera"))
.andExpect(status().isOk())
.andExpect(content().contentType("text/plain;charset=ISO-8859-1"))
.andExpect(content().string("ENI os dice hola"));
c. Revisión sencilla
El método simple2() especifica
que solo acepta consultas de tipo GET.
Código en la página:
<a id="simpleRevisited" class="textLink" href="<c:url
value="/primera/revisada" />">GET /primera/revisada</a>
En el controlador:
@RequestMapping(value="/primera/revisada",
method=RequestMethod.GET, headers="Accept=text/plain")
public @ResponseBody String simple2() {
return "ENI les desea buen provecho";
Pruebas unitarias:
@Test
public void simple2() throws Exception {
standaloneSetup(new PrimerControlador()).build()
.perform(get("/primera/revisada").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk())
.andExpect(content().contentType("text/plain"))
.andExpect(content().string("ENI les desea buen provecho"));
d. Por el path Path
El mapping se realiza en relación
con el path.
Código en la página:
<a id="byPath" class="textLink" href="<c:url
value="/mapping/path" />">Por el path</a>
En el controlador:
@RequestMapping("/mapping/path")
public @ResponseBody String byPath() {
return "Mapeo por path!";
Pruebas unitarias:
@Test
public void byPath() throws Exception {
this.mockMvc.perform(get("/mapping/path"))
.andExpect(content()
.string("Mapeo por path"));
En el controlador:
Con más opciones:
@RequestMapping(value="/mapping/path/*",
method=RequestMethod.GET)
public @ResponseBody String byPathPattern(HttpServletRequest request) {
return "Mapeado por el path pattern ('" + request.getRequestURI() + "')";
}
e. Por un patrón en el path
La anotación @RequestMapping también
procesa rutas de tipo Ant (por ejemplo, /myPath/*.do).
Los patrones URI se pueden mezclar con variables.
Código en la página:
<a id="byPathPattern" class="textLink" href="<c:url
value="/mapping/path/wildcard" />">Por un patrón sobre el path</a>
Pruebas unitarias:
@Test
public void byPathPattern() throws Exception {
this.mockMvc.perform(get("/mapping/path/wildcard"))
.andExpect(content().string("Mapeo por el path pattern
('/mapping/path/wildcard')"));
f. Por el path y un método
Podemos utilizar la URL para apuntar a un
método a través de un path con un nombre de método.
Código en la página:
<a id="byMethod" class="textLink" href="<c:url
value="/mapping/method" />">Por el path y un método</a>
En el controlador:
@RequestMapping(value="/mapping/method",
method=RequestMethod.GET)
public @ResponseBody String byMethod() {
return "Mapeo por path + método";
Pruebas unitarias:
@Test
public void byMethod() throws Exception {
this.mockMvc.perform(get("/mapping/method"))
.andExpect(content().string("Mapeado por path + método"));
g. Mapeado por path + método + presencia
de argumentos de query
Podemos pasar argumentos en la URL.
Código en la página:
<a id="byParameter" class="textLink" href="<c:url
value="/mapping/parameter?foo=bar" />">Mapeado por path + método +
presencia de argumentos de query</a>
En el controlador:
@RequestMapping(value="/mapping/parameter",
method=RequestMethod.GET, params="foo")
public @ResponseBody String byParameter() {
return "Mapeado por path + método + presencia de argumentos de
query";
Pruebas unitarias:
@Test
public void byParameter() throws Exception {
this.mockMvc.perform(get("/mapping/parameter?foo=bar"))
.andExpect(content().string("Mapeado por path + método +
presencia de argumentos de query"));
h. Mapeado por path + método + presencia
de un header
Podemos pasar argumentos en la URL y especificar
el método al que hay que llamar:
Código en la página:
<a id="byHeader" href="<c:url value="/mapping/header" />">Por la
presencia de un header</a>
En el controlador:
@RequestMapping(value="/mapping/header",
method=RequestMethod.GET, headers="FooHeader=foo")
public @ResponseBody String byHeader() {
return "Mapeado por path + método + presencia de un header!";
Pruebas unitarias:
@Test
public void byHeader() throws Exception {
this.mockMvc.perform(get("/mapping/header").header("FooHeader",
"foo"))
.andExpect(content().string("Mapeado por path + método + presencia
de un header"));
i. Por la ausencia de un header
Podemos llamar a un método si detectamos
la ausencia del header:
Código en la página:
<a id="byHeaderNegation" class="textLink" href="<c:url
value="/mapping/header" />">Por la ausencia de un header</a>
En el controlador:
@RequestMapping(value="/mapping/header",
method=RequestMethod.GET, headers="!FooHeader")
public @ResponseBody String byHeaderNegation() {
return "Mapeado por path + método + ausencia de un header";
Pruebas unitarias:
@Test
public void byHeaderNegation() throws Exception {
this.mockMvc.perform(get("/mapping/header"))
.andExpect(content().string("Mapeado por path + método + ausencia
de un header"));
j. Por consumo
Podemos tener un método de controlador
que consume un recurso, por ejemplo, un JSON:
Código en la página:
<form id="byConsumes" class="readJsonForm" action="
<c:url value="/mapping/consumes" />" method="post">
<input id="byConsumesSubmit" type="submit" value="Por consumo" />
</form>
En el controlador:
@RequestMapping(value="/mapping/consumes",
method=RequestMethod.POST,
consumes=MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody String byConsumes(@RequestBody JavaBean
javaBean) {
return "Mapeo por path + método + consumible por un tipo de
recurso multimedia (javaBean '" + javaBean + "')";
Pruebas unitarias:
@Test
public void byConsumes() throws Exception {
this.mockMvc.perform(
post("/mapping/consumes")
.contentType(MediaType.APPLICATION_JSON)
.content("{ \"foo\": \"bar\", \"fruit\": \"apple\" }".
getBytes()))
.andExpect(content().string(startsWith("Mapeo por path + método
+ consumible por un tipo de recurso multimedia (javaBean")));
k. Por producción a través de Accept=application/json
Podemos producir un recurso de tipo JSON en
un servicio REST JSON clásico: JSON
Código en la página:
<a id="byProducesAcceptJson" class="writeJsonLink" href="<c:url
value="/mapping/produces" />">Por producción a través de
Accept=application/json</a>
En el controlador:
@RequestMapping(value="/mapping/produces",
method=RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody JavaBean byProducesJson() {
return new JavaBean();
Pruebas unitarias:
@Test
public void byProducesAcceptJson() throws Exception {
this.mockMvc.perform(get("/mapping/produces").accept
(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.foo").value("bar"))
.andExpect(jsonPath("$.fruit").value("apple"));
l. Por producción a través de Accept=application/xml
Podemos pasar argumentos en la URL en formato
XML o JSON: XML
En la página para XML:
<a id="byProducesAcceptXml" class="writeXmlLink" href="<c:url
value="/mapping/produces" />">Por producción a través de
Accept=application/xml</a>
En el controlador:
@RequestMapping(value="/mapping/produces",
method=RequestMethod.GET,
produces=MediaType.APPLICATION_XML_VALUE)
public @ResponseBody JavaBean byProducesXml() {
return new JavaBean();
Pruebas unitarias:
@Test
public void byProducesAcceptXml() throws Exception {
this.mockMvc.perform(get("/mapping/produces")
.accept(MediaType.APPLICATION_XML))
.andExpect(xpath("/javaBean/foo").string("bar"))
.andExpect(xpath("/javaBean/fruit").string("apple"));
Código en la página para JSON:
JSON será la forma de hacer esto
para los servicios REST.
<a id="byProducesJsonExt" class="writeJsonLink" href="<c:url
value="/mapping/produces.json" />">By produces via ".json"</a>
Pruebas unitarias:
@Test
public void byProducesJsonExtension() throws Exception {
this.mockMvc.perform(get("/mapping/produces.json"))
.andExpect(jsonPath("$.foo").value("bar"))
.andExpect(jsonPath("$.fruit").value("apple"));
Código en la página XML (variante):
<a id="byProducesXmlExt" class="writeXmlLink" href="<c:url
value="/mapping/produces.xml" />">By produces via ".xml"</a>
Pruebas unitarias:
@Test
public void byProducesXmlExtension() throws Exception {
this.mockMvc.perform(get("/mapping/produces.xml"))
.andExpect(xpath("/javaBean/foo").string("bar"))
.andExpect(xpath("/javaBean/fruit").string("apple"));+
m. Argumentos de query
Podemos utilizar argumentos de consulta aislados:
Código en la página:
<a id="param" class="textLink" href="<c:url
value="/data/param?foo=bar" />">Argumentos de query</a>
En el controlador:
@RequestMapping(value="param", method=RequestMethod.GET)
public @ResponseBody String withParam(@RequestParam String foo) {
return "El valor del argumento de query 'foo' [" + foo + "]";
Pruebas unitarias:
@Test
public void param() throws Exception {
this.mockMvc.perform(get("/data/param?foo=bar"))
.andExpect(content().string("El valor del argumento de query
'foo' [bar]"));
n. Grupos de argumentos de consulta
Podemos utilizar múltiples argumentos
de consulta:
Código en la página:
<a id="group" class="textLink" href="<c:URL
value="/data/group?param1=foo¶m2=bar¶m3=baz"/>">Grupo
de argumentos de query</a>
En el controlador:
@RequestMapping(value="group", method=RequestMethod.GET)
public @ResponseBody String withParamGroup(JavaBean bean) {
return "El grupo de argumentos " + bean;
Pruebas unitarias:
@Test
public void group() throws Exception {
this.mockMvc.perform(get("/data/group?param1=
foo¶m2=bar¶m3=baz"))
.andExpect(content().string(startsWith(
"El grupo de argumentos
com.eni.spring.mvc.exemple.data.JavaBean@")));
o. Variable del path
Podemos usar el path para especificar una
variable y su valor:
Código en la página:
<a id="var" class="textLink" href="<c:url value="/data/path/foo"
/>">Variable del path</a>
En el controlador:
@RequestMapping(value="path/{var}", method=RequestMethod.GET)
public @ResponseBody String withPathVariable(@PathVariable String
var) {
return "El valor de la variable 'var' del path [" + var + "]";
Pruebas unitarias:
@Test
public void pathVar() throws Exception {
this.mockMvc.perform(get("/data/path/foo"))
.andExpect(content().string("El valor de la variable 'var' del
path [foo]"));
p. Cuerpo de consulta Consulta
Podemos usar el cuerpo de la request para el mapping:
Código en la página:
<form id="requestBody" class="textForm" action="<c:url
value="/data/body" />" method="post">
<input id="requestBodySubmit" type="submit" value="Cuerpo de la
consulta" />
</form>
En el controlador:
@RequestMapping(value="body", method=RequestMethod.POST)
public @ResponseBody String withBody(@RequestBody String body) {
return "Cuerpo de la consulta enviada: [" + body + "]";
Pruebas unitarias:
@Test
public void requestBody() throws Exception {
this.mockMvc.perform(
post("/data/body")
.contentType(MediaType.TEXT_PLAIN)
.content("foo".getBytes()))
.andExpect(content().string("Cuerpo de la consulta enviada:
[foo]"));
q. Encabezado y cuerpo de la consulta
También podemos usar la cabecera
y el cuerpo de la request.
Los datos transmitidos se pasan en el cuerpo.
Código en la página:
<form id="requestBodyAndHeaders" class="textForm" action="<c:url
value="/data/entity" />" method="post">
<input id="requestBodyAndHeadersSubmit" type="submit"
value="Cabecera y cuerpo de la consulta" />
</form>
En el controlador:
@RequestMapping(value="entity", method=RequestMethod.POST)
public @ResponseBody String withEntity(HttpEntity<String> entity) {
return " Cuerpo de la consulta enviada [" + entity.getBody() + "];
headers = " + entity.getHeaders();
}
Pruebas unitarias:
@Test
public void requestBodyAndHeaders() throws Exception {
this.mockMvc.perform(
post("/data/entity")
.contentType(MediaType.TEXT_PLAIN)
.content("foo".getBytes()))
.andExpect(content().string(
"Cuerpo de la consulta enviada [foo]; headers =
{Content-Type=[text/plain], Content-Length=[3]}"));
}
r. Argumentos en la consulta
Se pueden usar argumentos para la request.
Código en la página:
<a id="request" class="textLink" href="<c:url
value="/data/standard/request" />">Los argumentos de la consulta</a>
En el controlador:
@RequestMapping(value="/data/standard/request",
method=RequestMethod.GET)
public @ResponseBody String
standardRequestArgs(HttpServletRequest request, Principal user,
Locale locale) {
StringBuilder buffer = new StringBuilder();
buffer.append("request = ").append(request).append(", ");
buffer.append("userPrincipal = ").append(user).append(", ");
buffer.append("requestLocale = ").append(locale);
return buffer.toString();
Pruebas unitarias:
@Test
public void request() throws Exception {
this.mockMvc.perform(get("/data/standard/request"))
.andExpect(content().string(startsWith(
"request =
org.springframework.mock.web.MockHttpServletRequest@")));
}
s. Argumentos de la respuesta
Podemos trabajar con los argumentos de la
respuesta.
Código en la página:
<a id="response" class="textLink" href="<c:url
value="/data/standard/response" />">Los argumentos de la
respuesta</a>
En el controlador:
@RequestMapping("/data/standard/respuesta")
public @ResponseBody String response(HttpServletResponse respuesta) {
return "respuesta = " + respuesta;
Pruebas unitarias:
@Test
public void respuesta() throws Exception {
this.mockMvc.perform(get("/data/standard/respuesta"))
.andExpect(content().string(startsWith(
" respuesta =
org.springframework.mock.web.MockHttpServletResponse@")));
t. Sesión Sesión
Es posible recuperar los elementos de la sesión
como el SESSIONID con un controlador de tipo @RequestMapping("/data/standard/session"):
Código en la página:
<a id="session" class="textLink" href="<c:URL
value="/data/standard/session" />">Sesión</a>
En el controlador:
@RequestMapping("/data/standard/session")
public @ResponseBody String session(HttpSession session) {
StringBuilder buffer = new StringBuilder();
buffer.append("session=").append(session);
return buffer.toString();
Pruebas unitarias:
@Test
public void session() throws Exception {
this.mockMvc.perform(get("/data/standard/session"))
.andExpect(content().string(startsWith(
"session=org.springframework.mock.web.MockHttpSession@")));
u. Handler personalizado Handler
Es posible personalizar el handler de métodos
para realizar operaciones antes de invocarlo.
Código en la página:
<a id="customArg" class="textLink" href="<c:URL
value="/data/custom" />">Customizado</a>
En el controlador:
@ModelAttribute
void beforeInvokingHandlerMethod(HttpServletRequest request) {
request.setAttribute("foo", "bar");
@RequestMapping(value="/data/custom", method=RequestMethod.GET)
public @ResponseBody String custom(@RequestAttribute("foo") String foo) {
return "Obtención del valor del atributo 'foo'=[" + foo +
"]";
v. Leer los datos codificados en la URL
Podemos leer los datos codificados en la URL.
Código en la página:
<form id="readForm" action="<c:url
value="/conversordemensajes/form" />" method="post">
<input id="readFormSubmit" type="submit" value="Leer desde un Data" />
</form>
En el controlador:
@RequestMapping(value="/form", method=RequestMethod.POST)
public @ResponseBody String readForm(@ModelAttribute JavaBean bean) {
return "Lectura x-www-form-urlencoded: " + bean;
Pruebas unitarias:
@Test
public void readForm() throws Exception {
this.mockMvc.perform(
post(URI, "form")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("foo", "bar")
.param("fruit", "apple"))
.andExpect(content().string("Lectura x-www-form-
urlencoded: JavaBean {foo=[bar], fruit=[apple]}"));
w. Leer una estructura XML XML
Podemos recuperar los miembros de un objeto
en forma de estructura XML.
Código en la página:
<form id="readXml" class="readXmlForm" action="<c:url
value="/conversordemensajes/xml" />" method="post">
<input id="readXmlSubmit" type="submit" value="Leer una estructura XML" />
</form>
En el controlador:
that expect to work with XML)
@RequestMapping(value="/xml", method=RequestMethod.POST)
public @ResponseBody String readXml(@RequestBody JavaBean bean) {
return "Lectura desde XML: " + bean;
private static String XML =
"<?xml version=\"1.0\" encoding=\"UTF-8\"
standalone=\"yes\"?>" +
"<javaBean><foo>bar</foo><fruit>apple</fruit></javaBean>";
x. Escribir en una estructura XML a través de
Accept=application/xml
Podemos devolver una estructura XML que contenga
los miembros de un objeto.
Código en la página:
<a id="writeXmlAccept" class="writeXmlLink" href="<c:url
value="/conversordemensajes/xml" />">Escribir en una estructura
XML via Accept=application/xml</a>
En el controlador:
@RequestMapping(value="/xml", method=RequestMethod.GET)
public @ResponseBody JavaBean writeXml() {
return new JavaBean("bar", "apple");
Pruebas unitarias:
@Test
public void readXml() throws Exception {
this.mockMvc.perform(
post(URI, "xml")
.contentType(MediaType.APPLICATION_XML)
.content(XML.getBytes()))
.andExpect(content().string("Lectura desde XML:
JavaBean {foo=[bar], fruit=[apple]}"));
Código en la página:
<a id="writeXmlExt" class="writeXmlLink" href="<c:URL
value="/conversordemensajes/xml.xml" />">Write XML via
".xml"</a>
y. Leer una estructura JSON JSON
Podemos devolver una estructura JSON que contenga
los miembros de un objeto.
Código en la página:
<form id="readJson" class="readJsonForm" action="<c:url
value="/conversordemensajes/json" />" method="post">
<input id="readJsonSubmit" type="submit" value="Leer una estructura JSON" />
</form>
En el controlador:
JavaScript que piden trabajar con el formato JSON)
@RequestMapping(value="/json", method=RequestMethod.POST)
public @ResponseBody String readJson(@Valid @RequestBody JavaBean bean) {
return "Leer desde un estructura JSON: " + bean;
Pruebas unitarias:
@Test
public void writeXml() throws Exception {
this.mockMvc.perform(get(URI,
"xml").accept(MediaType.APPLICATION_XML))
.andExpect(content().xml(XML));
Código en la página:
<form id="readJsonInvalid" class="readJsonForm invalid"
action="<c:url value="/conversordemensajes/json" />"
method="post">
<input id="readInvalidJsonSubmit" type="submit" value="Leer una
estructura JSON inválida (código de respuesta 400)" />
</form>
El servicio devuelve 400 porque no es JSON.
z. Escribir en una estructura JSON a través
de Accept=application/json
Podemos devolver una estructura JSON que contenga
los miembros de un objeto.
Código en la página:
<a id="writeJsonAccept" class="writeJsonLink" href="<c:url
value="/conversordemensajes/json" />">Escribir en una
estructura JSON a través de Accept=application/json</a>
En el controlador:
@RequestMapping(value="/json", method=RequestMethod.GET)
public @ResponseBody JavaBean writeJson() {
return new JavaBean("bar", "apple");
Pruebas unitarias:
@Test
public void readJson() throws Exception {
this.mockMvc.perform(
post(URI, "json")
.contentType(MediaType.APPLICATION_JSON)
.content("{ \"foo\": \"bar\", \"fruit\": \"apple\"
}".getBytes()))
.andExpect(content().string("Leer desde un JSON: JavaBean
{foo=[bar], fruit=[apple]}"));
@Test
public void writeJson() throws Exception {
this.mockMvc.perform(get(URI,
"json").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.foo").value("bar"))
.andExpect(jsonPath("$.fruit").value("apple"));
aa. HTML generado por una plantilla JSP HTML JSP
Es posible crear una plantilla HTTP para las
JSP.
Código en la página:
<a href="<c:url value="/views/html" />">HTML generado por un
template JSP</a>
En el controlador:
@RequestMapping(value="html", method=RequestMethod.GET)
public String prepare(Model model) {
model.addAttribute("foo", "bar");
model.addAttribute("fruit", "apple");
return "views/html";
Pruebas unitarias:
@Test
public void htmlView() throws Exception {
this.mockMvc.perform(get("/views/html"))
.andExpect(view().nombre(containsString("views/html")))
.andExpect(model().attribute("foo", "bar"))
.andExpect(model().attribute("fruit", "apple"))
.andExpect(model().size(2));
ab. Mapping a partir de un modelo
Podemos usar DefaultRequestToViewNameTranslator.
Esto es muy práctico porque permite
hacer un mapping automático a partir de un modelo.
Código en la página:
<a href="<c:url value="/views/viewName"
/>">DefaultRequestToViewNameTranslator convention</a>
En el controlador:
@RequestMapping(value="/viewName", method=RequestMethod.GET)
public void usingRequestToViewNameTranslator(Model model) {
model.addAttribute("foo", "bar");
model.addAttribute("fruit", "apple");
Pruebas unitarias:
@Test
public void viewName() throws Exception {
this.mockMvc.perform(get("/views/viewName"))
.andExpect(view().nombre(containsString("views/viewName")))
.andExpect(model().attribute("foo", "bar"))
.andExpect(model().attribute("fruit", "apple"))
.andExpect(model().size(2));
ac. Usar variables en una plantilla de vista
Podemos usar variables en una plantilla de
vista utilizando la anotación @PathVariable.
Código en la página:
<a href="<c:url value="/views/pathVariables/bar/apple"
/>">Utilizar las variables en un template de vista </a>
En el controlador:
@RequestMapping(value="pathVariables/{foo}/{fruit}",
method=RequestMethod.GET)
public String pathVars(@PathVariable String foo, @PathVariable
String fruit) {
return "views/html";
Pruebas unitarias:
@Test
public void uriTemplate() throws Exception {
this.mockMvc.perform(get("/views/pathVariables/bar/apple"))
.andExpect(view().nombre(containsString("views/html")));
ad. Data binding con variables de URI Data binding URI
También podemos enlazar automáticamente
las variables de URI.
Código en la página:
<a href="<c:url value="/views/dataBinding/bar/apple" />">Data
binding con variables de URI</a>
En el controlador:
@RequestMapping(value="dataBinding/{foo}/{fruit}",
method=RequestMethod.GET)
public String dataBinding(@Valid JavaBean javaBean, Model model)
{
variables
return "views/dataBinding";
ae. Tipos primitivos Tipos:primitivos
Es posible mapear simplemente los tipos primitivos,
como las fechas. Los tipos primitivos a menudo plantean problemas
de interoperabilidad. En el siguiente ejemplo, se muestra código
anterior a Java 8 con Joda Time.
Código en la página:
<a id="primitive" class="textLink" href="<c:url
value="/convert/primitive?value=3" />">Primitivo</a>
Clase utilizada para mostrar los diferentes tipos primitivos:
Código simplificado:
public class JavaBean {
private Integer primitive;
@DateTimeFormat(iso=ISO.DATE)
private Date date;
@MaskFormat("(###) ###-####")
private String masked;
// list will auto-grow as its dereferenced e.g. list[0]=value
private List<Integer> list;
// annotation type conversion rule will be applied to each
list element
@DateTimeFormat(iso=ISO.DATE)
private List<Date> formattedList;
// map will auto-grow as its dereferenced e.g. map[key]=value
private Map<Integer, String> map;
// nested will be set when it is referenced e.g.
nested.foo=value
private NestedBean nested;
@Target(value={ElementType.FIELD, ElementType.METHOD,
ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MaskFormat {
String value();
public class MaskFormatAnnotationFormatterFactory implements
AnnotationFormatterFactory<MaskFormat> {
public Set<Class<?>> getFieldTypes() {
Set<Class<?>> fieldTypes = new HashSet<Class<?>>(1, 1);
fieldTypes.add(String.class);
return fieldTypes;
}
public Parser<?> getParser(MaskFormat annotation, Class<?>
fieldType) {
return new MaskFormatter(annotation.value());
}
public Printer<?> getPrinter(MaskFormat annotation, Class<?>
fieldType) {
return new MaskFormatter(annotation.value());
}
private static class MaskFormatter implements
Formatter<String> {
private javax.swing.text.MaskFormatter delegate;
public MaskFormatter(String mask) {
try {
this.delegate = new
javax.swing.text.MaskFormatter(mask);
this.delegate.setValueContainsLiteralCharacters(false);
} catch (ParseException e) {
throw new IllegalStateException("Mask could not be
parsed " + mask, e);
}
}
public String print(String object, Locale locale) {
try {
return delegate.valueToString(object);
} catch (ParseException e) {
throw new IllegalArgumentException("Unable to print
using mask " + delegate.getMask(), e);
}
}
public String parse(String text, Locale locale) throws
ParseException {
return (String) delegate.stringToValue(text);
}
}
public final class SocialSecurityNumber {
private final String value;
public SocialSecurityNumber(String value) {
this.value = value;
}
@MaskFormat("###-##-####")
public String getValue() {
return value;
}
public static SocialSecurityNumber valueOf(@MaskFormat("###-
##-####") String value) {
return new SocialSecurityNumber(value);
}
@RequestMapping("primitive")
public @ResponseBody String primitive(@RequestParam Integer
value) {
return "Conversión del tipo primitivo " + value;
Pruebas unitarias:
@Before
public void setup() throws Exception {
FormattingConversionService cs = new
DefaultFormattingConversionService();
cs.addFormatterForFieldAnnotation(new
MaskFormatAnnotationFormatterFactory());
this.mockMvc = standaloneSetup(new ConvertController())
.setConversionService(cs)
.alwaysExpect(status().isOk())
.build();
private String getTimezone(int year, int month, int day)
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month);
calendar.set(Calendar.DAY_OF_MONTH, day);
Date date = calendar.getTime();
TimeZone timezone = TimeZone.getDefault();
boolean inDaylight = timezone.inDaylightTime(date);
return timezone.getDisplayName(inDaylight, TimeZone.SHORT);
@Test
public void primitive() throws Exception {
this.mockMvc.perform(get("/convert/primitive").param("value", "3"))
.andExpect(content().string("Conversión del tipo primitivo 3"));
af. Fechas Fechas
Podemos decodificar fechas directamente:
Código en la página:
<a id="date" class="textLink" href="<c:URL
value="/convert/date/2010-07-04" />">Date</a>
En el controlador:
@RequestMapping("date/{value}")
public @ResponseBody String date(@PathVariable
@DateTimeFormat(iso=ISO.DATE) Date value) {
return "Fecha convertida " + value;
Pruebas unitarias:
@Test
public void date() throws Exception {
String timezone = getTimezone(2010, 7, 4);
this.mockMvc.perform(get("/convert/date/2010-07-04"))
.andExpect(content().string("Fecha convertida Sun Jul 04
00:00:00 " + timezone + " 2010"));
ag. Conversión de colecciones
A menudo, las colecciones son difíciles
de transmitir. Spring permite convertirlas automáticamente.
Código en la página:
<a id="collection" class="textLink" href="<c:url
value="/convert/collection?values=1&values=2&values=3&values=
4&values=5" />">Colección 1 (multi-value parameter)</a>
En el controlador:
@RequestMapping("collection")
public @ResponseBody String collection(@RequestParam
Collection<Integer> values) {
return "Colección convertida " + values;
Pruebas unitarias:
@Test
public void collection() throws Exception {
this.mockMvc.perform(get("/convert/collection?
values=1&values=2&values=3&values=4&values=5"))
.andExpect(content().string("Colección convertida [1, 2,
3, 4, 5]"));
<a id="collection2" class="textLink" href="<c:url
value="/convert/collection?values=1,2,3,4,5" />">Colección 2
(single comma-delimited parameter value)</a>
@Test
public void collection2() throws Exception {
this.mockMvc.perform(get("/convert/collection?values=1,2,3,4,5"))
.andExpect(content().string("Colección convertida [1, 2, 3, 4, 5]"));
ah. Usar colecciones con formato
Podemos utilizar colecciones formateadas.
Código en la página:
<a id="formattedCollection" class="textLink" href="<c:url
value="/convert/formattedCollection?values=2010-07-04,2011-07-04"
/>">@Formatted Collection</a>
En el controlador:
@RequestMapping("formattedCollection")
public @ResponseBody String formattedCollection(@RequestParam
@DateTimeFormat(iso=ISO.DATE) Collection<Date> values) {
return "Colección convertida formateada " + values;
ai. Trabajar con objetos personalizados
También tenemos la posibilidad de
utilizar objetos personalizados.
Código en la página:
<a id="valueObject" class="textLink" href="<c:url
value="/convert/value?value=123456789" />">Objeto personalizado</a>
En el controlador:
@RequestMapping("value")
public @ResponseBody String valueObject(@RequestParam
SocialSecurityNumber value) {
return "Valor del objeto convertido " + value;
Pruebas unitarias:
@Test
public void valueOf() throws Exception {
this.mockMvc.perform(get("/convert/value?value=123456789"))
.andExpect(content().string(startsWith(
"Valor del objeto convertido
com.eni.spring.mvc.exemple.convert.SocialSecurityNumber")));
aj. Usar un conversor personalizado
También tenemos los Converters para ayudarnos a cambiar
el formato de los datos.
Código en la página:
<a id="customConverter" class="textLink" href="<c:url
value="/convert/custom?value=123-45-6789" />">Custom
Converter</a>
En el controlador:
@RequestMapping("custom")
public @ResponseBody String customConverter(@RequestParam
@MaskFormat("###-##-####") String value) {
return "Convertido '"+ value +"' con un conversor personalizado";
Pruebas unitarias:
@Test
public void custom() throws Exception {
this.mockMvc.perform(get("/convert/custom?value=123-45-6789"))
.andExpect(content().string("Convertido '123456789' con un
conversor personalizado"));
Código en la página:
<a id="primitiveProp" class="textLink" href="<c:url
value="/convert/bean?primitive=3" />">Primitivo</a>
Pruebas unitarias:
@Test
public void beanPrimitive() throws Exception {
this.mockMvc.perform(get("/convert/bean?primitive=3"))
.andExpect(content().string("Conversor JavaBean
primitive=3"));
Código en la página:
<a id="dateProp" class="textLink" href="<c:url
value="/convert/bean?date=25-04-18" />">Fecha</a>
Pruebas unitarias:
@Test
public void beanDate() throws Exception {
String timezone = getTimezone(2010, 7, 4);
this.mockMvc.perform(get("/convert/bean?date=2010-07-04"))
.andExpect(content().string("Conversor JavaBean date=Sun
Jul 04 00:00:00 " + timezone + " 2010"));
Código en la página:
<a id="maskedProp" class="textLink" href="<c:url
value="/convert/bean?masked=(205) 333-3333" />">Masked</a>
Pruebas unitarias:
@Test
public void beanMasked() throws Exception {
this.mockMvc.perform(get("/convert/bean?masked=(205) 333-3333"))
.andExpect(content().string("Conversor JavaBean
masked=2053333333"));
Código en la página:
<a id="listProp" class="textLink" href="<c:url
value="/convert/bean?list[0]=1&list[1]=2&list[2]=3" />">List
Elements</a>
Pruebas unitarias:
@Test
public void beanCollection() throws Exception {
this.mockMvc.perform(get("/convert/bean?list[0]=1&list[1]=
2&list[2]=3"))
.andExpect(content().string("Conversor JavaBean list=
[1, 2, 3]"));
Código en la página:
<a id="formattedListProp" class="textLink" href="<c:url
value="/convert/bean?formattedList[0]=2010-07-
04&formattedList[1]=2011-07-04" />">@Formatted List Elements</a>
Pruebas unitarias:
@Test
public void beanFormattedCollection() throws Exception {
String timezone2010 = getTimezone(2010, 7, 4);
String timezone2011 = getTimezone(2011, 7, 4);
this.mockMvc.perform(get("/convert/bean?formattedList[0]=
25-04-18&formattedList[1]=25-04-18"))
.andExpect(content().string(
"Conversor JavaBean formattedList=[Sun Jul 04
00:00:00 " + timezone2010
+ " 2010, Mon Jul 04 00:00:00 " + timezone2011
+ " 2011]"));
<a id="mapProp" class="textLink" href="<c:url value="/convert/bean?
map[0]=apple&map[1]=pear" />">Map Elements</a>
@Test
public void beanMap() throws Exception {
this.mockMvc.perform(get("/convert/bean?map[0]=apple&map[1]=pear"
))
.andExpect(content().string("Conversor JavaBean
map={0=apple, 1=pear}"));
Código en la página:
<a id="nestedProp" class="textLink" href="<c:url
value="/convert/bean?nested.foo=bar&nested.list[0].foo=baz&nested
.map[key].list[0].foo=bip" />">Nested</a>
Pruebas unitarias:
@Test
public void beanNested() throws Exception {
this.mockMvc.perform(get("/convert/bean?
nested.foo=bar&nested.list[0].foo=baz&nested.map[key]
.list[0].foo=bip"))
.andExpect(content().string(
"Conversor JavaBean nested=NestedBean
foo=bar list=[NestedBean foo=baz] map={key=NestedBean
list=[NestedBean foo=bip]}"));
Código en la página:
<a id="validateNoErrors" class="textLink" href="<c:url
value="/validate?number=3&date=2029-07-04" />">Validate, no
errors</a>
Objeto para los ejemplos:
public class JavaBean {
@NotNull
@Max(5)
private Integer number;
@NotNull
@Future
@DateTimeFormat(iso=ISO.DATE)
private Date date;
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
Pruebas unitarias:
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(new
ValidationController()).alwaysExpect(status().isOk()).build();
@Test
public void validateSuccess() throws Exception {
this.mockMvc.perform(get("/validate?number=3&date=2029-07-04"))
.andExpect(content().string("Sin error "));
ak. Validación Validador
Se pueden utilizar validadores. Lo ideal es
validar en todos los niveles para evitar problemas, es decir, en
la página, el controlador, los servicios, las DAO y en la
base de datos, si es posible con restricciones de integridad.
Código en la página:
<a id="validateErrors" class="textLink" href="<c:url
value="/validate?number=3&date=25-04-18" />">Validate,
errors</a>
Es necesario agregar una gestión
de excepciones del tipo Business.
@SuppressWarnings("serial")
public class BusinessException extends Exception {
En el controlador:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public @ResponseBody String
handleBusinessException(BusinessException ex) {
return "Handled BusinessException";
}
que necesita una implementación de la JSR-303 en el classpath
@RequestMapping("/validate")
public @ResponseBody String validate(@Valid JavaBean bean,
BindingResult result) {
if (result.hasErrors()) {
return "El objeto tiene errores de validación";
} else {
return "Sin errores";
}
Pruebas unitarias:
@Test
public void validateErrors() throws Exception {
this.mockMvc.perform(get("/validate?number=3&date=25-04-18"))
.andExpect(content().string("El objeto tiene errores de
validación"));
al. @ExceptionHandler en un controlador
Es posible tener un administrador de excepciones
integrado en el controlador, usando la anotación @ExceptionHandler.
Código en la página:
<a id="exception" class="textLink" href="<c:URL
value="/exception" />">@ExceptionHandler en un controlador</a>
En el controlador:
@RequestMapping("/exception")
public @ResponseBody String exception() {
throw new IllegalStateException("Sorry!");
am. @ExceptionHandler global
Podemos tener un administrador de excepciones
integrado en el controlador.
Usaremos la anotación @ExceptionHandler.
Código en la página:
<a id="globalException" class="textLink" href="<c:URL
value="/global-exception" />">@ExceptionHandler global</a>
En el controlador:
@RequestMapping("/global-exception")
public @ResponseBody String businessException() throws
BusinessException {
throw new BusinessException();
an. Plantillas de String para las URI URI
Podemos usar plantillas URI.
Código en la página:
<a href="<c:url value="/redirect/uriTemplate" />">String de
Template de URI</a>
private final ConversionService conversionService;
@Inject
public RedirectController(ConversionService conversionService) {
this.conversionService = conversionService;
@RequestMapping(value="/uriTemplate", method=RequestMethod.GET)
public String uriTemplate(RedirectAttributes redirectAttrs) {
redirectAttrs.addAttribute("account", "a123");
una variable de template URI
redirectAttrs.addAttribute("date", new LocalDate(2011, 12, 31));
return "redirect:/redirect/{account}";
Pruebas unitarias:
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(new RedirectController(new
DefaultFormattingConversionService()))
.alwaysExpect(status().isMovedTemporarily()).build();
@Test
public void uriTemplate() throws Exception {
this.mockMvc.perform(get("/redirect/uriTemplate"))
.andExpect(redirectedUrl("/redirect/a123?
ao. UriComponentsBuilder UriComponentsBuilder
Tenemos a nuestra disposición el UriComponentsBuilder, que genera la URI
para nosotros.
Código en la página:
<a href="<c:url value="/redirect/uriComponentsBuilder"
/>">UriComponentsBuilder</a>
En el controlador:
@RequestMapping(value="/uriComponentsBuilder",
method=RequestMethod.GET)
public String uriComponentsBuilder() {
String date = this.conversionService.convert(new
LocalDate(2011, 12, 31), String.class);
UriComponents redirectUri =
UriComponentsBuilder.fromPath("/redirect/{account}")
.queryParam("date", date)
.build().expand("a123").encode();
return "redirect:" + redirectUri.toUriString();
Pruebas unitarias:
@Test
public void uriComponentsBuilder() throws Exception {
this.mockMvc.perform(get("/redirect/uriComponentsBuilder"))
.andExpect(redirectedUrl("/redirect/a123?date=12/31/11"));
@RequestMapping(value="/{account}", method=RequestMethod.GET)
public String show(@PathVariable String account,
@RequestParam(required=false) LocalDate date) {
return "redirect/redirectResults";