Código de ejemplo

JPetStore 6 es una aplicación web completa construida sobre MyBatis 3, Spring 4 y Stripes. Está disponible para su descarga en la sección de downloads del repositorio Github de MyBatis 3. En esta sección haremos un recorrido por este ejemplo para comprender cómo está construido y cómo ejecutarlo.

Propósito

Esta nueva versión de JPetStore viene con la misma idea en mente que sus precedesores: hazlo fácil. El propósito principal de JPetStore 6 es demostrar que una aplicación web completa puede construirse con sólo unas pocas clases, y lo que es más importante, sin necesidad de tener grandes conocimientos de programación. Sólo necesitas saber java básico y SQL.

La sexta versión de JPetStore es la más pequeña de la familia, un 20% más pequeña que su predecesora. Sólo usa 24 clases y conserva un buen diseño y estructura de programa.

eduardo@nomada ~ $ ./cloc-1.60.pl ~/git/jpetstore-6/src/main/
      60 text files.
      60 unique files.
       3 files ignored.

http://cloc.sourceforge.net v 1.60  T=0.28 s (209.8 files/s, 17722.9 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            24            480            462           1429
JSP                             20            148              0            984
XML                              9             79            120            405
CSS                              1             46              0            277
SQL                              2             26             30            226
HTML                             2             44              0            143
-------------------------------------------------------------------------------
SUM:                            58            823            612           3464
-------------------------------------------------------------------------------
      	

Como veremos un poco después, no encontrarás código JDBC, de creación de objetos, de enlace de objetos o de gestión de transacciones. Y lo que es más sorprendente es que ¡no encontrarás ninguna llamada al API de MyBatis!. A pesar de que esto suena a mágia, verás que la combinación de los mappers de MyBatis con la inyección de dependencias te permite realizar aplicaciones libres de dependencia de MyBatis.

Estructura del código

JPetStore 6 sigue la típica estructura de una aplicación maven.

/jpetstore                    <-- El fichero maven pom.xml va aqui.
  /src
    /main/
      /java                   <-- El código java va aqui.
        /org/
          /mybatis
            /jpetstore
              /domain         <-- Los objetos de negocio van aqui.
              /mapper         <-- Las mapper interfaces van aqui.
              /service        <-- La lógica de aplicación va aqui.
              /web
                /actions      <-- La lógica de presentación (actions) van aqui.
      /resources              <-- Aqui van los recursos no-java.
        /org
          /mybatis
            /jpetstore
              /mapper         <-- Los ficheros XML de mapeo van aqui.
        /database
      /webapp
        /css
        /images
        /WEB-INF              <-- web.xml y applicationContext.xml están aqui.
          /jsp                <-- los ficheros JSP van aqui.
      

Ficheros de configuración

Los ficheros de configuración se leen durante el arranque de la aplicación. Su propósito es configurar los tres frameworks que componen la aplicación: Stripes, Spring y MyBatis. Sólo tendremos que configurar dos ficheros: web.xml y applicationContext.xml.

web.xml

Primeramente debemos arrancar Stripes, asi que, para ello, seguimos el manual de Stripes. El manual indica que debe configurarse un dispatcher servlet y un filtro filter. Vamos allá.

<filter>
	<display-name>Stripes Filter</display-name>
	<filter-name>StripesFilter</filter-name>
	<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>StripesFilter</filter-name>
	<servlet-name>StripesDispatcher</servlet-name>
	<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<servlet>
	<servlet-name>StripesDispatcher</servlet-name>
	<servlet-class>net.sourceforge.stripes.controller.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>StripesDispatcher</servlet-name>
	<url-pattern>*.action</url-pattern>
</servlet-mapping>

Stripes es capaz de buscar los ActionBeans, para ello debemos configurar el paquete base desde el que debe comenzar la búsqueda.

<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
  <init-param>
    <param-name>ActionResolver.Packages</param-name>
    <param-value>org.mybatis.jpetstore.web</param-value>
  </init-param>
</filter>

Hemos acabado con Stripes. Continuemos con la parte de Spring. Según el manual de Spring debemos añadir un context listener para arrancar Spring asi que añadámoslo:

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Por defecto Spring usa el fichero de configuración /WEB-INF/applicationContext.xml si no indicamos uno distinto. El valor por defecto está bien para nosotros.

Ahora tenemos que decirle a Stripes que va a ejecutarse junto con Spring. De esta forma seremos capaces de inyectar beans de Spring en ActionBeans de Stripes. Para ello, siguiendo una vez más el manual de Stripes, configuramos un interceptor como sigue:

<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
  ...
  <init-param>
    <param-name>Interceptor.Classes</param-name>
    <param-value>net.sourceforge.stripes.integration.spring.SpringInterceptor</param-value>
  </init-param>
</filter>

Hemos acabado con el web.xml. Como habrás notado, no hemos realizado ninguna configuración de MyBatis 3 aun. Esa configuración va en el fichero applicationContext.xml de Spring y la veremos en la siguiente seccion. section.

applicationContext.xml

Como ya sabes el fichero applicationContext.xml es el fichero de configuración de Spring. Spring es un framework de inyección de dependencias y debe conocer qué beans debe crear y como enlazarlos y esto es precisamente para lo que sirve el applicationContext.xml. Echémosle un vistazo.

La primera tarea y la más sencilla que debemos hacer es indicarle a Spring donde buscar nuestros beans de servicio. Dejaremos que Spring los busque en nuestro classpath y para ello tenemos que indicar a Spring el paquete base donde comenzar la búsqueda:

<context:component-scan base-package="org.mybatis.jpetstore.service" />

NOTA Spring no es capaz de localizar de forma automática los mappers de MyBatis. Un mapper no es un bean normal y Spring no conocería cómo instanciarlos. Necesitaremos un MapperScannerConfigurer para esta tarea, como veremos pronto.

Necesitaremos también un DataSource y un TransactionManager. Como esto es una aplicación de demo usaremos un DataSource de test de Spring que crea una base de datos HSQL en memoria y carga en ella los scripts de datos, y el DataSourceTransactionManager estándar de Spring para gestionar transacciones.

<jdbc:embedded-database id="dataSource">
  <jdbc:script location="classpath:database/jpetstore-hsqldb-schema.sql"/>
  <jdbc:script location="classpath:database/jpetstore-hsqldb-dataload.sql"/>
</jdbc:embedded-database>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

Hasta ahora, todo lo que hemos hecho es configurar Stripes y Spring y ya es momento de movernos a la parte de MyBatis. Como ya has aprendido del manual de MyBatis para configurar MyBatis con Spring necesitas al menos dos cosas: un SqlSessionFactoryBean y un mapper. Asi que pongámonos manos a la obra. Primeramente definimos un SqlSessionFactoryBean:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

Y ahora tenemos que configurar nuestros mappers. Para ello vamos a usar un MapperScannerConfigurer que funciona de forma similar al component scan de Spring. El buscará mappers en nuestro classpath y los registrará en Spring. De forma similar a como hicimos con el component-scan de Spring debemos configurar un paquete base donde iniciar la búsqueda.

<mybatis:scan base-package="org.mybatis.jpetstore.mapper" />

Para escribir algo menos en nuestros ficheros de mapeo XML podemos utilizar alias cortos para nuestros beans. El SqlSessionFactoryBean tiene la capacidad de buscar beans y registrar sus nombres cortos como alias si configuramosla propiedad typeAliasPackage como sigue:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="typeAliasesPackage" value="org.mybatis.jpetstore.domain" />
</bean>

Nuestra aplicación está completamente configurada y lista para ejecutarse. Pero antes que eso demos un paseo por el código para ver qué pinta tiene.

Paseo por el código

JPetStore 6 es una aplicación MVC típica con tres capas: presentación, logica y acceso a datos.

Presentación

La capa de presentación está compuesta por ficheros JSP y ActionBeans de Stripes. Los JSPs usan HTML simple, tags JSTL y tagas de Stripes asi que no hay nada especial sobre ellos de cara a este ejemplo. Los ActionBeans de Stripes son como los actions de Struts o los controllers de Spring MVC asi que tampoco hay nada especial acerca de ellos.

Dado que hemos integrado Stripes con Spring, podemos inyectar nuestros servcios en nuestros ActionBeans de forma que podemos simplemente usarlos sin preocuparnos de crearlos o buscarlos. Echa un vistazo al CatalogActionBean:

@SessionScope
public class CatalogActionBean extends AbstractActionBean {
  ...
  @SpringBean
  private transient CatalogService catalogService;
  ...
  public ForwardResolution viewCategory() {
    if (categoryId != null) {
      productList = catalogService.getProductListByCategory(categoryId);
      category = catalogService.getCategory(categoryId);
    }
    return new ForwardResolution(VIEW_CATEGORY);
  }
  ...

Fíjate que en la anotación @SpringBean annotation, que es una anotación de Stripes que le indica a Stripes que busque este bean en Spring y lo inyecte en el ActionBean.

Lógica

La lógica de aplicación está compuesta de Java beans planos que actuan como servicios y Java beans planos que actúan como objetos de dominio. Esta capa se encarga de rellenar objetos de dominio con datos de base de datos y de modificar la base de datos con el contenido de estos mismos objetos. Para ello esta capa debe ser transaccional, esto es, debe ser capaz de ralizar modificaciones atómicas en la base de datos.

Veamos la clase OrderService para ver cómo hemos hecho todo esto:

@Service
public class OrderService {

  @Autowired
  private ItemMapper itemMapper;
  @Autowired
  private OrderMapper orderMapper;
  @Autowired
  private LineItemMapper lineItemMapper;

  @Transactional
  public void insertOrder(Order order) {
    order.setOrderId(getNextId("ordernum"));
    for (int i = 0; i < order.getLineItems().size(); i++) {
      LineItem lineItem = (LineItem) order.getLineItems().get(i);
      String itemId = lineItem.getItemId();
      Integer increment = new Integer(lineItem.getQuantity());
      Map<String, Object> param = new HashMap<String, Object>(2);
      param.put("itemId", itemId);
      param.put("increment", increment);
      itemMapper.updateInventoryQuantity(param);
    }

    orderMapper.insertOrder(order);
    orderMapper.insertOrderStatus(order);
    for (int i = 0; i < order.getLineItems().size(); i++) {
      LineItem lineItem = (LineItem) order.getLineItems().get(i);
      lineItem.setOrderId(order.getOrderId());
      lineItemMapper.insertLineItem(lineItem);
    }
  }

Lo primero que habrás notado es que no hay código JDBC en el servicio y tampoco código de MyBatis. Puedes pensar que hemos usado el patron DAO y que el código de acceso a base de datos está en la capa de acceso a datos, pero como veremos después, la capa de acceso a base de datos está construida con mappers de MyBatis, que son interfaces java simples, y este es el motivo por el que no encontraás ninguna llamada al API de MyBatis en toda la aplicación. Simplemente no es necesario.

Lo segundo en lo que te puedes haber fijado es que no hay commits ni rollbacks. Esto es porque el código usa la demarcación de transacciones declarativa de Spring que se soporta completamente en MyBatis-Spring. La anotación @Transactional de Spring indica que este método es transaccional, lo que significa que todas las llamadas a mappers updateInventoryQuantity, insertOrder and insertLineItem deben finalizar con éxito. En caso de que alguna de ellas falle, todas las actualizaciones que se hubieran hecho antes se desharán.

Persistencia

La capa de persistencia está compuesta por mappers de MyBatis. Los mappers son interfaces Java simples y ficheros XML que contienen las sentencias SQL. No hay código Java en esta capa. Cuando se ejecuta el método getOrder del mapper interface OrderMapper, MyBatis ejecuta la sentencia SQL del stamtent getOrder del ficheroOrderMapper.xml file y rellena el bean de dominio Order con los datos obtenidos.

public interface OrderMapper {
  List<Order> getOrdersByUsername(String username);
  Order getOrder(int orderId);
  void insertOrder(Order order);
  void insertOrderStatus(Order order);
}
<mapper namespace="org.mybatis.jpetstore.mapper.OrderMapper">

  <cache />

  <select id="getOrder" resultType="Order" parameterType="int">
    SELECT
      BILLADDR1 AS billAddress1,
      BILLADDR2 AS billAddress2,
      BILLCITY,
      BILLCOUNTRY,
      BILLSTATE,
      BILLTOFIRSTNAME,
      BILLTOLASTNAME,
      BILLZIP,
      SHIPADDR1 AS shipAddress1,
      SHIPADDR2 AS shipAddress2,
      SHIPCITY,
      SHIPCOUNTRY,
      SHIPSTATE,
      SHIPTOFIRSTNAME,
      SHIPTOLASTNAME,
      SHIPZIP,
      CARDTYPE,
      COURIER,
      CREDITCARD,
      EXPRDATE AS expiryDate,
      LOCALE,
      ORDERDATE,
      ORDERS.ORDERID,
      TOTALPRICE,
      USERID AS username,
      STATUS
    FROM ORDERS, ORDERSTATUS
    WHERE ORDERS.ORDERID = #{value}
      AND ORDERS.ORDERID = ORDERSTATUS.ORDERID
  </select>
  ...
</mapper>

NOTE Puedes añadir caché a tus queries fácilmente añadiendo un elemento <cache /> a tu fichero de mapeo XML.

Running JPetStore

Te prenguntarás. ¿Esto funciona? ¡Sí! Ejecutémoslo.

Asumiendo que tienes un PC limpio estos son los pasos que debes seguir para ejecutar el ejemplo en Tomcat.

Ya estás listo para jugar con la demo, experimentar tus propios cambios o lo que tú quieras.

Y recuerda que si encuentras un bug o ves que falta algo o que hay algo mejorable (por ejemplo faltan los tests!), haz un fork del repositorio, cámbialo y abre un pull request. Gracias de antemano!

NOTE JPetStore y debe ejecutarse en cualquier servidor compatible Servlet 2.5 y JSP 2.1. Tampoco es necesario NetBeans o Eclipse, puedes ejecutarlo desde tu IDE favorito o desde la línea de comando.