Passo a passo para desenvolver uma aplicação usando o Spring Framework MVC – Capitulo 6 – Integrando a aplicação Web e a camada de persistência

Essa é a Sexta e última parte do passo a passo de como desenvolver uma aplicação web do zero usando o framework Spring. Na Parte 1, configuramos o ambiente e uma aplicação básica. Na Parte 2, refinamos a aplicação que construímos. A Parte 3 adicionou a lógica de negócios e as unidades de teste e na Parte 4 construímos a interface web da aplicação. Agora é a vez de introduzir a persistência do baco de dados. Na Parte 5 desenvolvemos a camada de persistência. Agora é o momento de integrar tudo em uma aplicação web completa.

6.1. Modificar a camada de serviço

Se nós estruturamos a nossa aplicação de forma correta, deveriamos apenas ter que mudar as classes da camada de serviço para tirar a vantagem da persistência de banco de dados. As classes da visão e do controlador não devem ser modificadas, já que elas não devem tomar conhecimento de qualquer detalhe da implementação da camada de serviço. Assim, vamos adicionar a persistência na implementação do ProductManager. Nós modificaremos a SimpleProductManager e adicione uma referência a interface ProductDAO mais um método setter para essa referência. Esta implementação que nós usamos aqui deve ser irrelavante a classe do ProductManager, e iremos configurar ela através de uma opção de configuração. Também mudaremos o método setProduct para um método setProductDso, assim poderemos injetar uma instância da classe DAO. O método getProducts não usará DAO para recuperar a lista de produtos. Finalmente, o método increasePrices pegará a lista de produtos e depois deles terão seu preço aumentado serão armazenados no banco de dados usando o método saveProduct no DAO.

'springapp/src/springapp/service/SimpleProductManager.java':

package springapp.service;

import java.util.List;

import springapp.domain.Product;

public class SimpleProductManager implements ProductManager {

privapackage springapp.service;

import java.util.List;

import springapp.domain.Product;
import springapp.repository.ProductDao;

public class SimpleProductManager implements ProductManager {

// private List<Product> products;
private ProductDao productDao;

public List<Product> getProducts() {
// return products;
return productDao.getProductList();
}

public void increasePrice(int percentage) {
List<Product> products = productDao.getProductList();
if (products != null) {
for (Product product : products) {
double newPrice = product.getPrice().doubleValue() *
(100 + percentage)/100;
product.setPrice(newPrice);
productDao.saveProduct(product);
}
}
}

public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}

// public void setProducts(List<Product> products) {
// this.products = products;
// }

}

6.2. Corrigir os testes

Re-escrevemos o SimpleProductManager e agora os testes irão naturalmente falhar. Precisamos fornecer o ProductManager com um implementação totalmente na memória de ProductDao. Nós náo queremos usar um DAO real já que gostariamos de evitar acessar um banco de dados em nossas unidades de testes. Adicionaremos uma classe interna chamada InMemoryProductDao que guardará uma lista de produtos fornecidos pelo construtor. Essa classe na memória tem que ser passada como parâmetro quando criamos uma nova instância SimpleProductManager.

'springapp/test/springapp/repository/InMemoryProductDao.java':
package springapp.repository;

import java.util.List;

import springapp.domain.Product;

public class InMemoryProductDao implements ProductDao {

private List<Product> productList;

public InMemoryProductDao(List<Product> productList) {
this.productList = productList;
}

public List<Product> getProductList() {
return productList;
}

public void saveProduct(Product prod) {
}

}

E aqui está a classe SimpleProductManagerTests modificada:

'springapp/test/springapp/service/SimpleProductManagerTests.java':
package springapp.service;

import java.util.ArrayList;
import java.util.List;

import springapp.domain.Product;
import springapp.repository.InMemoryProductDao;
import springapp.repository.ProductDao;

import junit.framework.TestCase;

public class SimpleProductManagerTests extends TestCase {

private SimpleProductManager productManager;

private List<Product> products;

private static int PRODUCT_COUNT = 2;

private static Double CHAIR_PRICE = new Double(20.50);
private static String CHAIR_DESCRIPTION = "Chair";

private static String TABLE_DESCRIPTION = "Table";
private static Double TABLE_PRICE = new Double(150.10);

private static int POSITIVE_PRICE_INCREASE = 10;

protected void setUp() throws Exception {
productManager = new SimpleProductManager();
products = new ArrayList<Product>();

// stub up a list of products
Product product = new Product();
product.setDescription("Chair");
product.setPrice(CHAIR_PRICE);
products.add(product);

product = new Product();
product.setDescription("Table");
product.setPrice(TABLE_PRICE);
products.add(product);

ProductDao productDao = new InMemoryProductDao(products);
productManager.setProductDao(productDao);
//productManager.setProducts(products);
}

public void testGetProductsWithNoProducts() {
productManager = new SimpleProductManager();
productManager.setProductDao(new InMemoryProductDao(null));
assertNull(productManager.getProducts());
}

public void testGetProducts() {
List<Product> products = productManager.getProducts();
assertNotNull(products);
assertEquals(PRODUCT_COUNT, productManager.getProducts().size());

Product product = products.get(0);
assertEquals(CHAIR_DESCRIPTION, product.getDescription());
assertEquals(CHAIR_PRICE, product.getPrice());

product = products.get(1);
assertEquals(TABLE_DESCRIPTION, product.getDescription());
assertEquals(TABLE_PRICE, product.getPrice());
}

public void testIncreasePriceWithNullListOfProducts() {
try {
productManager = new SimpleProductManager();
productManager.setProductDao(new InMemoryProductDao(null));
productManager.increasePrice(POSITIVE_PRICE_INCREASE);
}
catch(NullPointerException ex) {
fail("Products list is null.");
}
}

public void testIncreasePriceWithEmptyListOfProducts() {
try {
productManager = new SimpleProductManager();
productManager.setProductDao(new InMemoryProductDao(new ArrayList<Product>()));
//productManager.setProducts(new ArrayList<Product>());
productManager.increasePrice(POSITIVE_PRICE_INCREASE);
}
catch(Exception ex) {
fail("Products list is empty.");
}
}

public void testIncreasePriceWithPositivePercentage() {
productManager.increasePrice(POSITIVE_PRICE_INCREASE);
double expectedChairPriceWithIncrease = 22.55;
double expectedTablePriceWithIncrease = 165.11;

List<Product> products = productManager.getProducts();
Product product = products.get(0);
assertEquals(expectedChairPriceWithIncrease, product.getPrice());

product = products.get(1);
assertEquals(expectedTablePriceWithIncrease, product.getPrice());
}

}
We also need to modify the InventoryControllerTests since that class also uses the SimpleProductManager. Here is the modified InventoryControllerTests:

'springapp/test/springapp/service/InventoryControllerTests.java':
package springapp.web;

import java.util.Map;

import org.springframework.web.servlet.ModelAndView;

import springapp.domain.Product;
import springapp.repository.InMemoryProductDao;
import springapp.service.SimpleProductManager;
import springapp.web.InventoryController;

import junit.framework.TestCase;

public class InventoryControllerTests extends TestCase {

public void testHandleRequestView() throws Exception{
InventoryController controller = new InventoryController();
SimpleProductManager spm = new SimpleProductManager();
spm.setProductDao(new InMemoryProductDao(new ArrayList<Product>()));
controller.setProductManager(spm);
//controller.setProductManager(new SimpleProductManager());
ModelAndView modelAndView = controller.handleRequest(null, null);
assertEquals("hello", modelAndView.getViewName());
assertNotNull(modelAndView.getModel());
Map modelMap = (Map) modelAndView.getModel().get("model");
String nowValue = (String) modelMap.get("now");
assertNotNull(nowValue);
}
}

6.3. Crie um novo contexto de aplicação para a camada de serviço

Vimos mais cedo que é bem fácil modificar a camada de serviço para usar a persistência do banco de dados. Isso se deve a desacoplação da camada da web. Agora é o momento de desacoplar a configuração da camada de serviço assim como da camada de serviço. Não removeremos a configuração do ProductManager e a lista de produtos da configuração do arquivo springapp-servlet.xml. Abaixo segue como esse arquivo deve ficar:

'springapp/war/WEB-INF/springapp-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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<!-- the application context definition for the springapp DispatcherServlet -->

<bean id="messageSource">
<property name="basename" value="messages"/>
</bean>

<bean name="/hello.htm">
<property name="productManager" ref="productManager"/>
</bean>

<bean name="/priceincrease.htm">
<property name="sessionForm" value="true"/>
<property name="commandName" value="priceIncrease"/>
<property name="commandClass" value="springapp.service.PriceIncrease"/>
<property name="validator">
<bean/>
</property>
<property name="formView" value="priceincrease"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

<bean id="viewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

</beans>

Nós ainda precisamos configurar a camada de serviço e faremos isso em seu próprio arquivo de contexto de aplicação. Esse arquivo é chamado ‘applicationContext.xml’ e será carregado por um ouvinte de servlet que será definido em ‘web.xml’. Todos os beans configurados nessa novo contexto de aplicação estará disponível para referência em qualquer contexto de servlet.

'springapp/war/WEB-INF/web.xml':
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >

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

<servlet>
<servlet-name>springapp</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>springapp</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>
index.jsp
</welcome-file>
</welcome-file-list>

<jsp-config>
<taglib>
<taglib-uri>/spring</taglib-uri>
<taglib-location>/WEB-INF/tld/spring-form.tld</taglib-location>
</taglib>
</jsp-config>

</web-app>

Agora criamos um novo arquivo ‘applicationContext.xml’ no diretório ‘war/WEB-INF’:

'springapp/war/WEB-INF/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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

<!-- the parent application context definition for the springapp application -->

<bean id="productManager">
<property name="productDao" ref="productDao"/>
</bean>

<bean id="productDao">
<property name="dataSource" ref="dataSource"/>
</bean>

</beans>

6.4. Adicione um pool de transações e conexão ao contexto da aplicação

Toda vez que você persiste dados no banco de dados é melhor usar transações para garantir que todas as suas atualizações sejam executadas ou nenhuma seja completa. Você quer evitar que metade de suas atualizações seja gravada e a outra não. O Spring fornece uma gama de opções extensa para fornecer gerenciamento de transações. O manual de referência cobre essa parte em profundidade. Aqui iremos fazer uso de uma maneira de fornecer esse rexursos usando AOP (Aspect Oriented Programming) na forma de um aviso de transação e um ponto de corte do AspectJ para definir onde as transações devem ser aplicadas. Se você estiver interessado em saber como isso funciona com mais detalhes, dê uma olhada no manual de referência. Estamos usando o novo suporte a namespace do Spring 2.0. Os namespaces “aop” e “tx” fazem as entradas da configuração muitas mais concisas comparadas a tradicional maneira usando entradas <bean> comuns.
<bean id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<aop:config>
<aop:advisor pointcut="execution(* *..ProductManager.*(..))" advice-ref="txAdvice"/>
</aop:config>

<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="save*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>

Os pontos de corte de aplicam a qualquer método chamado na interface ProductManager. O aviso é um aviso transacional que aplica-se ao método com um nome que começe como ‘save’. A transação padrão de REQUIRED é aplicada quando nenhum outro atributo é especificado. O avido também é aplicado em transações somente leitura ou qualquer outro método que é avisado por pontos de corte.

Tambpem precisamos definir um pool de conexão. Estaremos usando o pool de  conexão  DBCP do projeto Apache Jakarta. Vamos re-usar o arquivo jdbc.properties da Parte 5.
<bean id="dataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<bean id="propertyConfigurer"
>
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
</list>
</property>
</bean>

Para tudo isso funcionar precisamos de alguns arquivos jar adicionais npo diretório WEB-INF/lib. Copie aspectjweaver.jar do diretório 'spring-framework-2.5/lib/aspectj' e commons-dbcp.jar e commons-pool.jar de 'spring-framework-2.5/lib/jakarta-commons' para 'springapp/war/WEB-INF/lib'.

Abaixo está a versão final de nosso arquivo ‘applicationContext.xml’:

'springapp/war/WEB-INF/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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

<!-- the parent application context definition for the springapp application -->

<bean id="productManager">
<property name="productDao" ref="productDao"/>
</bean>

<bean id="productDao">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="dataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<bean id="propertyConfigurer"
>
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
</list>
</property>
</bean>

<bean id="transactionManager"
>
<property name="dataSource" ref="dataSource"/>
</bean>

<aop:config>
<aop:advisor pointcut="execution(* *..ProductManager.*(..))" advice-ref="txAdvice"/>
</aop:config>

<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="save*"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>

</beans>

6.5. Teste final da aplicação completa

Agora finalmente chegou o momento de todas as peças funcionarão juntas. Compile e carregue nossa aplicação finalizada e lembre de iniciar o banco de dados.  Isso é o que você deve ver quando apontar o navegador para a aplicação depois de ter carregado-a:

Parece o mesmo que vimos antes. Como nós implementamos persistência, se você reiniciar a sua aplicação suas alterações no preço não serão perdidas. Eles ainda estarão lá quando você iniciar a aplicação.

Um monte de trabalho para uma aplicação simples foi feito, mas nunca foi nosso objetivo escrever essa aplicação. O objetivo foi mostrar cpo,p escrever uma aplicação Spring MVC do zero e sabemos que as aplicações que criaremos serão muito mais complexas. Os mesmos passos aplicam-se a elas e esperamos que você tenha obtido conhecimento suficiente para tornar mais fácil seu uso do Spring.

6.6. Sumário

Nós completamos todas as três camadas da aplicação – web, serviço e persistência. Nessa última parte nós re-configuramos a aplicação.

  1. Primeiro modificamos a camada de serviço para usar ProductDao.
  2. Depois corrigimos alguns serviços que deram erro e os testes da camada web.
  3. Em seguida introduzimos um novo applicationContext para separar as configurações das camadas de serviço e persistência  da confiuguração da camada web.
  4. Também definimos alguns gerenciadores de transações para a camada de serviço e configuramos um pool de conexão para as conexões com o banco de dados.
  5. Finalmente compilamos a aplicação reconfigurada e testamos para verificar que tudo estava funcionando.

Abaixo segue a tela de como deve ficar o diretório de nosso projeto depois de todas essas alterações.