Proyecto de Integración
 

Migración del proyecto a Spring. Parte II: la capa web/REST

Capa web: proyecto jbib-web-spring

Lo primero que vamos a hacer es configurar el proyecto para incluir las dependencias de Spring y configurarlo adecuadamente.

Cambios en la configuración del pom.xml

Lo primero es modificar el pom.xml. Además de cambiar el name y artifactId añadiéndoles el "-spring" y también el nombre del padre por proyint-jbib-spring, necesitamos añadir unas cuantas dependencias del API de Spring:


<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-web</artifactId>
	<version>3.0.6.RELEASE</version>
</dependency>

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-webmvc</artifactId>
	<version>3.0.6.RELEASE</version>
</dependency>

Además tenemos que modificar la dependencia del proyecto de negocio para que ahora sea de "jbib-negocio-spring".

Configuración del contenedor de Spring

Necesitamos un fichero de configuración de beans para los beans de la capa de negocio y de acceso a datos. Lo crearemos en WEB-INF y lo llamaremos applicationContext.xml

¿Otro fichero de configuración de beans?
Hay que tener en cuenta que los que ya teníamos en los proyectos anteriores eran para los test, no eran para la aplicación "en producción". Aunque para esto podríamos haber puesto un .xml por cada módulo, vamos a configurar todos los beans aquí porque los otros módulos no están pensados para ser "independientes".

El applicationContext.xml contendrá los siguientes datos:


<?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:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:p="http://www.springframework.org/schema/p"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-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/jee 
          http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
          http://www.springframework.org/schema/tx 
          http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

    <context:component-scan 
       base-package="org.especialistajee.jbib"/>

    <bean id="miEMF" 
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="proyint"/>
    </bean>

    <tx:annotation-driven transaction-manager="miTransactionManager" />

    <bean id="miTransactionManager" 
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="miEMF" />
    </bean>
</beans>

Nótese que se parece mucho a los que empleábamos para los test (lógico, ya que los beans son los mismos, se usen para test o producción). La diferencia básica es la unidad de persistencia (ahora "proyint") y el EntityManagerFactoryBean o gestor de EMFs. Ahora usamos otra clase distinta, pensada para ser ejecutada en un servidor web como Tomcat. La que usábamos antes era solo adecuada para pruebas.

Para terminar la configuración todavía nos queda la capa web. Necesitaremos modificar el web.xml para redirigir todas las peticiones *.do al dispatcher servlet de Spring.


<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>*.do</url-pattern>
</servlet-mapping>

Y crear otro fichero de configuración de beans de Spring (sí, otro), este para la capa web y en WEB-INF/dispatcher-servlet.xml. Recordemos que el nombre por defecto que asume Spring es el dado al servlet dispatcher con el sufijo "-servlet.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: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/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="org.jbib.web" />
	<mvc:annotation-driven />

	<bean id="viewResolver"
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/jsp/" />
		<property name="suffix" value=".jsp" />
	</bean>
</beans>    


Nótese que en el "view resolver" tenemos en cuenta que nuestras páginas tienen extensión ".jsp" y están en el subdirectorio "/jsp". Por tanto los controllers se limitarán a devolver el nombre de la página, sin trayectoria ni extensión.

Convertir los servlets en controllers

Lo que en JavaEE son servlets, en Spring son Controllers. Por convención vamos a renombrar todas las clases *Servlet en *Controller. Evidentemente tendremos que cambiar más cosas:

  • Anotaremos los controllers con @Controller
  • Podemos aprovechar el contenido del método doGet. Eso sí, tendremos que cambiar su signatura por algo como String procesar() y lo anotaremos con un @RequestMapping con la URL pertinente. Nótese que la anotación equivalente en servlets es @WebServlet, aunque en los servlets se pone en la clase.
  • procesar() recibirá los parámetros que nos hagan falta para la tarea en cuestión. Recordad que:
    • Para acceder a un parámetro HTTP podemos poner un parámetro Java en procesar() y anotarlo con @RequestParam("nombre_del_parametro_http")
    • Para devolver datos al JSP, definiremos en procesar()un parámetro de tipoModel y le añadiremos los datos con addAttribute, el equivalente al setAttribute de los servlets.
    • Si necesitamos acceso al request, session, ... podemos obtenerlo sin más que definir un parámetro en procesar() del tipo adecuado. Por ejemplo, nos servirá para invalidar la sesión en el logout.
    • Si queremos acceder a varios parámetros HTTP producto de rellenar un formulario (alta de libro), usaremos un JavaBean como parámetro de procesar().
  • Finalmente, no debemos crear los objetos de negocio que necesitemos con new(), sino definirlos como variables de instancia y anotarlos con @Autowired para que los instancie Spring.

Como ejemplo se muestra el resultado de la conversión de ListarLibrosServlet en ListarLibrosController. Por cierto, tendremos que cambiar el welcome-file del web.xml para que apunte al controller (o sea, a la URL ListarLibrosController.do).

@Controller
public class ListarLibrosController {
	private static final long serialVersionUID = 1L;

	static Log logger = LogFactory.getLog(ListarLibrosController.class);

	@Autowired
	ILibroBo ilb;

	@RequestMapping("ListarLibrosController")
	public String procesar(Model modelo, HttpServletRequest request) {
		try {
			System.out.println("Estoy en ListarLibrosController");
			List<LibroDomain> lista = ilb.listaLibros();
			modelo.addAttribute("listaLibros", lista);
			if(request.isUserInRole(TipoUsuario.BIBLIOTECARIO.name())) {
				return "biblio/listadoLibros";
			} else if(request.isUserInRole(TipoUsuario.PROFESOR.name()) ||
					request.isUserInRole(TipoUsuario.ALUMNO.name())) {
				return "usuario/listadoLibros";
			} else {
				modelo.addAttribute("error", 
				                    "Pagina no disponible para el usuario actual");
				return "error";
			}
		} catch (BibliotecaException ex) {
			modelo.addAttribute("error",
					"Error obteniendo el listado de libros. " + ex);
			logger.error("Error obteniendo el listado de libros. " + ex);
			return "error";
		}
	}
}

Haced los cambios equivalentes para el resto de servlets. Los casos de "alta/edición de libro" son especiales por la validación de datos, por lo que se explicará más en detalle en el siguiente apartado. Por otro lado, aunque como hemos visto en el módulo de Spring un solo Controller puede responder a varias URL, se aconseja transformar cada servlet en un controller distinto para evitar confusiones en la refactorización.

Controllers con validación de datos: alta/edición de libro

Se recomienda hacerlos en dos "pasadas":

  1. En una primera refactorización simplemente convertiremos el servlet en Controller y en lugar de leer los datos directamente del request los leeremos de un parámetro de procesar() de tipo LibroDomain.
  2. Tras comprobar que esto funciona:
    • Anotaremos LibroDomain con las restricciones: el nº de páginas debe ser positivo y el isbn no puede estar vacío. Para esto último usaremos la anotación @NotBlank,ya que @NotNull nos daría problemas si el usuario teclea espacios en el isbn (no sería válido pero tampoco sería null). Tendremos que incluir en el pom.xml de jbib-modelo-spring la dependencia de Hibernate Validator:
      
      <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-validator</artifactId>
          <version>4.2.0.Final</version>
      </dependency>
      
      
      Cuidado con las dependencias inapropiadas
      Esta solución no es muy "limpia" ya que nos obliga a anotar la clase de dominio e introducir una nueva dependencia en el proyecto "modelo". La alternativa sería crear un Transfer Object en el proyecto de Spring, anotar éste y en el controller copiar los datos en el LibroDomain (con BeanUtils, por ejemplo). No obstante, JSR303 no es un API de Spring, es un estándar, y parece apropiado que un LibroDomain no pueda tener un nº negativo de páginas ni un isbn en blanco, estemos usando Spring o no. En cualquier caso, es una decisión de diseño.
    • Anotaremos el parámetro LibroDomain de procesar() con @Valid, para decirle a Spring que queremos "disparar" la validación.
  3. Cambiaremos el formulario de alta/edición por tags de Spring. Para que Spring resuelva correctamente la correspondencia formulario/parámetro de procesar(), recordad que valor del atributo "modelAttribute" del formulario debe coincidir con el de la anotación @ModelAttribute para el parámetro LibroDomain.

Servicios REST: proyecto jbib-rest-spring

Vamos a hacer un proyecto REST muy reducido, no vamos a implementar todas las funcionalidades que tenéis en el REST que usa Jersey. Nuestro objetivo es que tengáis una base de código simple para comparar ambos enfoques.

Configuración

Lo primero, como siempre, es cambiar manualmente el pom.xml. Además de cambiar el name y artifactId añadiéndoles el "-spring" y también el nombre del padre por proyint-jbib-spring, hay que modificar las dependencias. Eliminaremos las dependencias jersey-server y jersey-json y añadiremos la de spring-web, spring-mvc y jackson

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-web</artifactId>
	<version>3.0.6.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-webmvc</artifactId>
	<version>3.0.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.6.4</version>
</dependency>

En el web.xml, hay que eliminar el servlet de jersey (Jersey Web Application) y cambiarlo por el dispatcher servlet de Spring. Vamos a mapearlo con la misma URL que usábamos: resources/*

<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>/resources/*</url-pattern>
</servlet-mapping>

Además, también en el web.xml hay que añadir el listener para el fich. de configuración de las capas bo y modelo. Esto será lo que arrancará Spring en estas capas.

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
	<listener-class>
	   org.springframework.web.context.ContextLoaderListener
	</listener-class>
</listener>
<listener>
	<listener-class>
		org.springframework.web.context.request.RequestContextListener
	</listener-class>
</listener>

Finalmente, hay que crear los fichs de configuración de beans de spring, applicationContext.xml (capa de negocio y modelo) y dispatcher-servlet.xml (capa web) . El primero es exactamente igual que en el proyecto spring-web. El segundo también será igual al equivalente del proyecto web de Spring, quitando el viewResolver, que ya no hace falta, y modificando el component-scan para que busque anotaciones de Spring en el paquete org.jbib.rest.resources.

Implementación de los servicios REST

Para intercambiar información entre cliente y servidor mantendremos las clases LibroBean (un solo libro) y LibrosBean (una lista de libros), con algunas modificaciones. Como no vamos a implementar HATEOAS, eliminaremos LinkBean y todo lo que tenga que ver con links en las dos clases anteriores (el campo "links", el método "addLink", etc.). Además, debemos elminar todas las anotaciones JAXB.

Vamos a implementar únicamente el listado de libros y la inserción de un nuevo libro. Recordad que estas funcionalidades estaban en la clase LibrosResource. Tendremos que hacer las siguientes modificaciones: (cuidado, estas son normas genéricas, tendréis que pensar un poco cómo hacer el cambio de Jersey a Spring)

  • Anotar la clase con @Controller
  • Donde antes usábamos @Path ahora usaremos @RequestMapping
  • Donde antes usábamos @GET para especificar el método HTTP, ahora lo haremos con @RequestMapping(method=RequestMethod.GET) (idem para POST)
  • Si en Jersey se accede a los parámetros HTTP con @QueryParam y se les da un valor por defecto con @DefaultValue, en Spring se usa @RequestParam y el valor por defecto es un parámetro de la anotación:
    • Por ejemplo @QueryParam("maxResults") @DefaultValue("0") int maxResults, pasa a ser en Spring @RequestParam(defaultValue="0") int maxResults
  • Para métodos que reciben datos: el equivalente al @Consumes sería anotar el parámetro con @RequestBody. El formato se tomará automáticamente de la configuración (en nuestro caso JSON).
  • Para métodos que envían datos: El método hay que anotarlo con @ResponseBody.
  • Usar inyección de dependencias con @Autowired en vez de factorías para acceder a los objetos de negocio
  • Si necesitamos acceso a la petición, la respuesta o el usuario logueado actualmente (el Principal), no tenemos más que definirnos parámetros del tipo adecuado y Spring nos inyectará los objetos. No se usa ninguna anotación especial para esto.

Como ejemplo se os da ya implementado el método de insertar libro. Por tanto solo os faltaría implementar el listado de libros al acceder con GET a la misma URI "libros"

@Controller
@RequestMapping("/libros")
public class LibrosResource {

 @Autowired
 ILibroBo ilb;


 @RequestMapping(method=RequestMethod.POST)
 public void addLibro(HttpServletRequest request, HttpServletResponse response,
		Principal principal, @RequestBody LibroBean libro) {
	if (principal==null) {
		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
	} else if(!request.isUserInRole(TipoUsuario.BIBLIOTECARIO.toString())){
		response.setStatus(HttpServletResponse.SC_FORBIDDEN);
	} else {
		ilb.anyadeLibro(libro.toLibroDomain());
		response.setHeader("Location", 
		          request.getRequestURL()+"/"+libro.getIsbn());
	}		
 }	
 
}

Para probar el envío de libros puedes usar este HTML (es un .txt, renómbralo a .html y cópialo a src/main/webapp). Rellena el formulario con los datos del nuevo libro.

Entrega

Para la entrega la dirección del proyecto padre proyint-jbib-spring en el servidor SVN debe ser:

svn+ssh://server.jtech.ua.es/home/svn/<grupo>/trunk/proyint-jbib-spring