Diferencia entre revisiones de «REST con Spring»
(→Ver también) |
(→Descargar un proyecto de ejemplo) |
||
(No se muestran 14 ediciones intermedias de 5 usuarios) | |||
Línea 199: | Línea 199: | ||
} | } | ||
</code> | </code> | ||
+ | |||
+ | == Manejo de errores == | ||
+ | Es posible personalizar las excepciones para que devuelvan códigos de estado HTTP personalizados, usando la anotación @ResponseStatus en la excepción lanzada. | ||
+ | |||
+ | <code java5> | ||
+ | @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No se encontró el recurso pedido") | ||
+ | public class NotFoundException extends RuntimeException { ... } | ||
+ | |||
+ | ..... | ||
+ | @Controller | ||
+ | @RequestMapping(value="/persona") | ||
+ | public class PersonaController { | ||
+ | @RequestMapping(value="/id/{id}") | ||
+ | public @ResponseBody Persona findById(@PathVariable long id) { | ||
+ | Contact contact = personaService.findById(id); | ||
+ | if (contact == null) { | ||
+ | throw new NotFoundException(); | ||
+ | } else { | ||
+ | return contact; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | Otra forma de personalizar las excepciones, es interceptar las excepciones del controller(@Controller) y personalizarla en el mismo controller. | ||
+ | Para esto debemos crear un método handleException por cada excepción que deseemos personalizar. | ||
+ | |||
+ | <code java5> | ||
+ | @Controller | ||
+ | @RequestMapping(value="/persona") | ||
+ | public class PersonaController { | ||
+ | @RequestMapping(value="/id/{id}") | ||
+ | public @ResponseBody Persona findById(@PathVariable long id) { | ||
+ | Contact contact = personaService.findById(id); | ||
+ | if (contact == null) { | ||
+ | throw new NotFoundException(); | ||
+ | } else { | ||
+ | return contact; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | @ResponseStatus(HttpStatus.NOT_FOUND) | ||
+ | @ExceptionHandler(NotFoundException.class) | ||
+ | @ResponseBody | ||
+ | public Map<String, String> handleException(NotFoundException exception) { | ||
+ | return Collections.singletonMap("message", "No se encontró el recurso pedido"); | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Y agregar lo siguiente en el applicationContext.xml | ||
+ | <code xml> | ||
+ | <?xml version="1.0" encoding="UTF-8"?> | ||
+ | <beans xmlns="http://www.springframework.org/schema/beans" | ||
+ | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
+ | xmlns:p="http://www.springframework.org/schema/p" | ||
+ | xmlns:aop="http://www.springframework.org/schema/aop" | ||
+ | xmlns:tx="http://www.springframework.org/schema/tx" | ||
+ | xmlns:context="http://www.springframework.org/schema/context" | ||
+ | xmlns:mvc="http://www.springframework.org/schema/mvc" | ||
+ | xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd | ||
+ | http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd | ||
+ | http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd | ||
+ | http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd | ||
+ | http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> | ||
+ | |||
+ | <context:component-scan base-package="com.dosideas.jsonconspringmvc"/> | ||
+ | |||
+ | <mvc:annotation-driven /> | ||
+ | <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver"> | ||
+ | <property name="messageConverters"> | ||
+ | <list> | ||
+ | <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" /> | ||
+ | </list> | ||
+ | </property> | ||
+ | </bean> | ||
+ | </beans> | ||
+ | </code> | ||
+ | Al utilizar este interceptor se debe personalizar las excepciones en todos los controladores, caso contrario la excepción no es customizada y retorna un status code 500. | ||
+ | La propiedad "messageConverters", define que conversor utilizar en el ResponseBody, en nuestro caso lo estamos pasando a Json. | ||
+ | |||
+ | En caso de que se quiera utilizar los 2 métodos, el de @ResponseStatus y el de @ExceptionHandler, se deben registrar los 2 Handler en el applicationContext.xml con el orden de ejecución. | ||
+ | <code xml> | ||
+ | ... | ||
+ | <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver"> | ||
+ | <property name="order" value="1"/> | ||
+ | <property name="messageConverters"> | ||
+ | <list> | ||
+ | <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" /> | ||
+ | </list> | ||
+ | </property> | ||
+ | </bean> | ||
+ | |||
+ | <bean class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver" > | ||
+ | <property name="order" value="2"/> | ||
+ | </bean> | ||
+ | ... | ||
+ | </code> | ||
+ | |||
+ | == Manejo de la respuesta == | ||
+ | La anotación @ResponseStatus puede usarse para personalizar el código HTTP de cualquier respuesta, al ubicarla en el método. | ||
+ | Por ejemplo, los métodos que devuelven void pueden usar esta anotación para indicar que no hay contenido (es útil si, por ejemplo, se parsea el contenido con algunas librerías como [[JQuery]]). | ||
+ | |||
+ | <code java5> | ||
+ | @RequestMapping(value="/delete/{id}") | ||
+ | @ResponseStatus(HttpStatus.NO_CONTENT) | ||
+ | public void delete(@PathVariable long id) { | ||
+ | contactService.delete(id); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | === Descargar un proyecto de ejemplo === | ||
+ | Les dejamos para [http://www.dosideas.com/descargas/category/2-spring-framework.html?download=41%3Ap descargar un proyecto de ejemplo de REST con Spring MVC] donde tienen todos los fuentes necesarios para abrir y ejecutar el ejemplo de esta página. | ||
+ | |||
+ | === Test con componentes mock de un cliente REST=== | ||
+ | * [[Mock REST con Spring]] | ||
== Ver también == | == Ver también == | ||
* [http://www.dosideas.com/descargas/category/2-spring-framework.html?download=41%3Ap Descargar un proyecto de ejemplo] | * [http://www.dosideas.com/descargas/category/2-spring-framework.html?download=41%3Ap Descargar un proyecto de ejemplo] | ||
+ | * [[Restfuse]] - testing de servicios REST. | ||
+ | * [[Spring Test MVC]] - testing de controladores de Spring. | ||
* [http://static.springsource.org/spring/docs/3.0.0.M3/spring-framework-reference/html/ch18.html Creating Restful services - Documentación oficial] | * [http://static.springsource.org/spring/docs/3.0.0.M3/spring-framework-reference/html/ch18.html Creating Restful services - Documentación oficial] | ||
* [http://www.informit.com/guides/content.aspx?g=java&seqNum=544 Creating Restful web services] | * [http://www.informit.com/guides/content.aspx?g=java&seqNum=544 Creating Restful web services] | ||
* [http://www.ibm.com/developerworks/web/library/wa-spring3webserv/index.html?ca=drs- Building RESTful web services using Spring 3] | * [http://www.ibm.com/developerworks/web/library/wa-spring3webserv/index.html?ca=drs- Building RESTful web services using Spring 3] | ||
+ | * [http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=springRestJson Spring + REST + JSON = SOAUI] | ||
+ | * [http://www.jpalace.org/docs/spring/rest.html Sprint REST Tutorial] | ||
+ | |||
+ | |||
+ | |||
+ | [[Category:Spring MVC]] |
Revisión actual del 15:27 23 oct 2012
Spring Framework 3.x trae la posibilidad de crear servicios web REST de manera muy simple.
Contenido
Un ejemplo
Vamos a realizar una aplicación que responda a las siguientes URL:
/persona/todos : devuelve todas las personas /persona/123 : devuelve la persona cuyo id es 123
Tendremos que seguir 3 pasos:
- Configurar el servlet de Spring
- Crear la clase que atenderá y resolverá estas peticiones
- Configurar Spring
El servlet de Spring
Lo primero es configurar el servlet de Spring que se encargará de tomas las peticiones y redireccionarla a las clases correspondientes. La clase que se encarga de esto es org.springframework.web.servlet.DispatcherServlet. Un archivo web.xml de ejemplo sería:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>json-con-spring-mvc</display-name>
<filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter>
<filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<servlet> <servlet-name>Spring MVC Dispatcher Servlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>
WEB-INF/applicationContext-clasico.xml
</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
<servlet-mapping> <servlet-name>Spring MVC Dispatcher Servlet</servlet-name> <url-pattern>/app/*</url-pattern> </servlet-mapping>
<session-config> <session-timeout> 30 </session-timeout> </session-config> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
</web-app>
En este ejemplo, el servlet de Spring queda atendiendo todas las URL que empiecen con "/app".
El controller
La clase que toma las peticiones HTTP es una clase Java anotada con @Controller. Esta clase asocia las URL con métodos. Los objetos que devuelven los métodos son luego procesador por conversores, que representan la respuesta de distinta manera (XML, JSON, etc.)
Para esto, creamos una clase de la siguiente forma:
package com.dosideas.jsonconspringmvc;
import java.util.ArrayList; import java.util.Collection; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;
@Controller @RequestMapping(value="/persona") public class PersonaController {
@RequestMapping(value="/{id}") public @ResponseBody Persona get(@PathVariable Long id) { Persona p = new Persona("Invasor " + id, "Zim " + id); return p; }
@RequestMapping(value="/todos") public @ResponseBody Collection<Persona> getTodos() { Collection<Persona> personas = new ArrayList<Persona>(); for (int i = 0; i < 10; i++) { Persona p = new Persona("Invasor " + i, "Zim" + i); personas.add(p); }
return personas; }
}
El objeto Persona es un POJO que tiene dos atributos: nombre y apellido. Los tags @RequestMapping se encargan de asociar la clase y cada uno de sus métodos con una URL en particular.
La configuración
Lo último que nos queda configurar es Spring con un archivo clásico (el cual es referenciado por el servlet):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> <context:component-scan base-package="com.dosideas.jsonconspringmvc"/>
<mvc:annotation-driven />
</beans>
El tag mvc:annotation-driven se encarga de configurar automáticamente para emepezar a recibir y responder peticiones usando los controladores encontrados.
Más aún, la configuración predeterminada se encarga de realizar negociación de contenido: la respuesta se formatea de acuerdo a las capacidades del cliente. Por ejemplo, si en el classpath tenemos agregado las librerías de Jackson (un parser JSON), se habilitará el soporte JSON correspondiente. Si están agregadas las librerías de JAXB2, se habilitará el soporte XML.
Probando
Podemos crear una prueba JUnit para acceder a nuestro servicio:
package com.dosideas.jsonconspringmvc;
import java.io.IOException; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpClient; import org.junit.Test; import static org.junit.Assert.*;
public class HttpTest {
@Test public void personaTodosJSON() throws IOException { HttpClient httpclient = new HttpClient(); HttpMethod method = new GetMethod("http://localhost:8084/json-con-spring-mvc/app/persona/todos"); method.addRequestHeader("accept", "application/json");
httpclient.executeMethod(method); String resultado = method.getResponseBodyAsString(); System.out.println(resultado);
assertNotNull(resultado); assertTrue(resultado.startsWith("[{\"")); }
@Test public void personaPorIdJSON() throws IOException { HttpClient httpclient = new HttpClient(); HttpMethod method = new GetMethod("http://localhost:8084/json-con-spring-mvc/app/persona/34421"); method.addRequestHeader("accept", "application/json");
httpclient.executeMethod(method); String resultado = method.getResponseBodyAsString(); System.out.println(resultado);
assertNotNull(resultado); assertTrue(resultado.startsWith("{\"")); assertTrue(resultado.contains("34421")); }
@Test public void personaPorIdXML() throws IOException { HttpClient httpclient = new HttpClient(); HttpMethod method = new GetMethod("http://localhost:8084/json-con-spring-mvc/app/persona/34421"); method.addRequestHeader("accept", "application/xml");
httpclient.executeMethod(method); String resultado = method.getResponseBodyAsString(); System.out.println(resultado);
assertNotNull(resultado); assertTrue(resultado.startsWith("<?xml version=\"1.0\"")); assertTrue(resultado.contains("34421")); }
}
Manejo de errores
Es posible personalizar las excepciones para que devuelvan códigos de estado HTTP personalizados, usando la anotación @ResponseStatus en la excepción lanzada.
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No se encontró el recurso pedido")
public class NotFoundException extends RuntimeException { ... }
..... @Controller @RequestMapping(value="/persona") public class PersonaController {
@RequestMapping(value="/id/{id}") public @ResponseBody Persona findById(@PathVariable long id) { Contact contact = personaService.findById(id); if (contact == null) { throw new NotFoundException(); } else { return contact; } }
}
Otra forma de personalizar las excepciones, es interceptar las excepciones del controller(@Controller) y personalizarla en el mismo controller. Para esto debemos crear un método handleException por cada excepción que deseemos personalizar.
@Controller
@RequestMapping(value="/persona")
public class PersonaController {
@RequestMapping(value="/id/{id}") public @ResponseBody Persona findById(@PathVariable long id) { Contact contact = personaService.findById(id); if (contact == null) { throw new NotFoundException(); } else { return contact; } } @ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler(NotFoundException.class) @ResponseBody public Map<String, String> handleException(NotFoundException exception) { return Collections.singletonMap("message", "No se encontró el recurso pedido"); }
}
Y agregar lo siguiente en el applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd"> <context:component-scan base-package="com.dosideas.jsonconspringmvc"/>
<mvc:annotation-driven /> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver"> <property name="messageConverters">
<list> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" /> </list> </property>
</bean>
</beans> Al utilizar este interceptor se debe personalizar las excepciones en todos los controladores, caso contrario la excepción no es customizada y retorna un status code 500. La propiedad "messageConverters", define que conversor utilizar en el ResponseBody, en nuestro caso lo estamos pasando a Json.
En caso de que se quiera utilizar los 2 métodos, el de @ResponseStatus y el de @ExceptionHandler, se deben registrar los 2 Handler en el applicationContext.xml con el orden de ejecución.
...
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver">
<property name="order" value="1"/> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" /> </list> </property>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver" >
<property name="order" value="2"/>
</bean> ...
Manejo de la respuesta
La anotación @ResponseStatus puede usarse para personalizar el código HTTP de cualquier respuesta, al ubicarla en el método. Por ejemplo, los métodos que devuelven void pueden usar esta anotación para indicar que no hay contenido (es útil si, por ejemplo, se parsea el contenido con algunas librerías como JQuery).
@RequestMapping(value="/delete/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable long id) { contactService.delete(id); }
Descargar un proyecto de ejemplo
Les dejamos para descargar un proyecto de ejemplo de REST con Spring MVC donde tienen todos los fuentes necesarios para abrir y ejecutar el ejemplo de esta página.
Test con componentes mock de un cliente REST
Ver también
- Descargar un proyecto de ejemplo
- Restfuse - testing de servicios REST.
- Spring Test MVC - testing de controladores de Spring.
- Creating Restful services - Documentación oficial
- Creating Restful web services
- Building RESTful web services using Spring 3
- Spring + REST + JSON = SOAUI
- Sprint REST Tutorial