Passo a passo para desenvolver uma aplicação usando o Spring Framework MVC – Capitulo 4 – Desenvolvendo a Interface Web

Essa é a Parte 4 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. Agora é o momento de construir a interface web da aplicação.

4.1. Adicionando referência à lógica de negócio no Controlador

Em primeiro lugar, vamos renomaear nosso HelloController para algo mais útil como InventoryController, já que estamos construindo um sistema de inventário. Aqui é onde uma IDE com suporte a refatoração é um item precioso. Renomeamos HelloController para InventoryController e HelloControllerTests para InventoryControllerTests. Em seguida, modificamos InventoryController para fazer referência a classe ProductManager. Também adicionamos código para que o controlador passe algumas informações de produto para as visões. O método getModelAndView() agora retorna um Map com data e hora e uma lista de produtos obtida da referência ao gerenciador.

'springapp/src/springapp/web/InventoryController.java':

package springapp.web;

import org.springframework.web.servlet.mvc.Controller;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Map;
import java.util.HashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;

public class InventoryController implements Controller {

    protected final Log logger = LogFactory.getLog(getClass());

    private ProductManager productManager;

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String now = (new java.util.Date()).toString();
        logger.info("returning hello view with " + now);

        Map<String, Object> myModel = new HashMap<String, Object>();
        myModel.put("now", now);
        myModel.put("products", this.productManager.getProducts());

        return new ModelAndView("hello", "model", myModel);
    }

    public void setProductManager(ProductManager productManager) {
        this.productManager = productManager;
    }

}

Também precisamos modificar o InventoryControllerTests para fornecer um ProductManager e extrair o valor de ‘now’ do modelo Map antes que os testes passem novamente.

'springapp/test/springapp/web/InventoryControllerTests.java':

package springapp.web;

import java.util.Map;

import org.springframework.web.servlet.ModelAndView;

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();
        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);
    }
}

4.2. Modifique a visão para exibir os dados do negócio e adicione suporte para empacotamento de mensagens

Usando a tag JSTL <c:forEach/>, adicionamos uma seção que exibe informações dos produtos. Também trocamos o titulo, cabeçalho e texto de boas vindas com a tag JSTL <fmt:message/> que pega o texto a exibir de uma fornte ‘message’ fornecida – nós mostraremos essa fonte em um passo mais adiante.

'springapp/war/WEB-INF/jsp/hello.jsp':

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<html>
  <head><title><fmt:message key="title"/></title></head>
  <body>
    <h1><fmt:message key="heading"/></h1>
    <p><fmt:message key="greeting"/> <c:out value="${model.now}"/></p>
    <h3>Products</h3>
    <c:forEach items="${model.products}" var="prod">
      <c:out value="${prod.description}"/> <i>$<c:out value="${prod.price}"/></i><br><br>
    </c:forEach>
  </body>
</html>

4.3. Adicione alguns dados de teste para automaticamente popular alguns objetos de negócio

Nesse momento vamos adicionar um SimpleProductManaget em nosso arquivo de configuração e passar ele ao setter de InventoryController. Não iremos adicionar nenhuma código para carregar objetos de negócios de uma base de dados ainda. Ao invés disso, vamos fornecer algumas instâncias de Product usando o bean do Spring e o suporte a contexto da aplicação. Nós simplesmente colocamos os dados que precisamos como um par de entradas em ‘springapp-servlet.xml’. Tambpem adicionaremos o bean ‘messageSource’ que pegará as mensagens do empacotador que criaremos no próximo passo. Também devemos lembrar de a referencia à HelloController já que nós renomeamos ela.

'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="productManager">
        <property name="products">
            <list>
                <ref bean="product1"/>
                <ref bean="product2"/>
                <ref bean="product3"/>
            </list>
        </property>
    </bean>

    <bean id="product1">
        <property name="description" value="Lamp"/>
        <property name="price" value="5.75"/>
    </bean>

    <bean id="product2">
        <property name="description" value="Table"/>
        <property name="price" value="75.25"/>
    </bean>

    <bean id="product3">
        <property name="description" value="Chair"/>
        <property name="price" value="22.79"/>
    </bean>

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

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

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

</beans>

4.4. Adicione o empacotador de mensagens e um alvo limpo ao ‘build.xml'

Criaremos um arquivo ‘messages.properties’ no diretório ‘war/WEB-INF/classes’. Esse empacotador de propriedades até agora tem três propriedades que batem com as chaves especificadas nas tags <fmt:message/> que adicionamos a ‘hello.jsp’.

'springapp/war/WEB-INF/classes/messages.properties':

title=SpringApp
heading=Hello :: SpringApp
greeting=Greetings, it is now

Since we moved some source files around, it makes sense to add a 'clean' and an 'undeploy' target to the build script. We add the following entries to the 'build.xml' file.

'build.xml':

    <target name="clean" description="Clean output directories">
        <delete>
            <fileset dir="${build.dir}">
                <include name="**/*.class"/>
            </fileset>
        </delete>
    </target>

    <target name="undeploy" description="Un-Deploy application">
        <delete>
            <fileset dir="${deploy.path}/${name}">
                <include name="**/*.*"/>
            </fileset>
        </delete>
    </target>

Agora vamos parar o servidor Tomcat, executar os alvos ‘clean’, ‘undeploy’ e ‘deploy’. Isso removerá todas as classes antigas, re-compilará a aplicação e carregará-la. Inicie o Tomcat novamente e você deve ver o seguinte.

4.5. Adicionando um formulário

Para fornecer uma interface na aplicação web para expor a funcionalidade de aumento do preço, nós iremos adicionar um formulário que permitirá que o usuário entre com um valor percentual. Esse formulário usa uma biblioteca chamada ‘spring-form.tld’ que é fornecida com o framework Spring. Temos que copiar esse arquivo da distribuição do Spring ('spring-framework-2.5/dist/resources/spring-form.tld') para o diretório 'springapp/war/WEB-INF/tld' que também precisamos criar. Em seguida precisamos adicionar um entrada <taglib/> no arquivo ‘web.xml’.

'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" >

  <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>

Também temos que declarar essa taglib em uma diretiva de página no arquivo jsp, e depois usar as tags que acabamos de importar. Adicione uma página JSP ‘priceincrease.jsp’ no diretório ‘war/WEB-INF/jsp’.

'springapp/war/WEB-INF/jsp/priceincrease.jsp':

<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
  <title><fmt:message key="title"/></title>
  <style>
    .error { color: red; }
  </style>
</head>
<body>
<h1><fmt:message key="priceincrease.heading"/></h1>
<form:form method="post" commandName="priceIncrease">
  <table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
    <tr>
      <td align="right" width="20%">Increase (%):</td>
        <td width="20%">
          <form:input path="percentage"/>
        </td>
        <td width="60%">
          <form:errors path="percentage" cssClass="error"/>
        </td>
    </tr>
  </table>
  <br>
  <input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

Esse próxima classe é uma classe JavaBean muito simples, e em nosso caso existe uma única propriedade como um par de métodos getter e setter. Esse é o objeto que o formulário irá popular e que a nossa lógica de negócio irá extrair o percentual de aumento de preço.

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

package springapp.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PriceIncrease {

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    private int percentage;

    public void setPercentage(int i) {
        percentage = i;
        logger.info("Percentage set to " + i);
    }

    public int getPercentage() {
        return percentage;
    }

}

A classe validadora a seguir assumi o controle das ações quando o usuário clica no botão ‘submit’. Os valores fornecidos no formulário serão inserido no objeto de comando pelo framework, O método validate(..) é chamado e o objeto de comando (PriceIncrease) e um objeto contextual que armazena qualquer erro que ocorra é passado.

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

package springapp.service;

import org.springframework.validation.Validator;
import org.springframework.validation.Errors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PriceIncreaseValidator implements Validator {
    private int DEFAULT_MIN_PERCENTAGE = 0;
    private int DEFAULT_MAX_PERCENTAGE = 50;
    private int minPercentage = DEFAULT_MIN_PERCENTAGE;
    private int maxPercentage = DEFAULT_MAX_PERCENTAGE;

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    public boolean supports(Class clazz) {
        return PriceIncrease.class.equals(clazz);
    }

    public void validate(Object obj, Errors errors) {
        PriceIncrease pi = (PriceIncrease) obj;
        if (pi == null) {
            errors.rejectValue("percentage", "error.not-specified", null, "Value required.");
        }
        else {
            logger.info("Validating with " + pi + ": " + pi.getPercentage());
            if (pi.getPercentage() > maxPercentage) {
                errors.rejectValue("percentage", "error.too-high",
                    new Object[] {new Integer(maxPercentage)}, "Value too high.");
            }
            if (pi.getPercentage() <= minPercentage) {
                errors.rejectValue("percentage", "error.too-low",
                    new Object[] {new Integer(minPercentage)}, "Value too low.");
            }
        }
    }

    public void setMinPercentage(int i) {
        minPercentage = i;
    }

    public int getMinPercentage() {
        return minPercentage;
    }

    public void setMaxPercentage(int i) {
        maxPercentage = i;
    }

    public int getMaxPercentage() {
        return maxPercentage;
    }

}

4.6. Adicionando um controlador de formulário

Agora precisamos adicionar uma entrada no arquivo ‘springapp-servlet.xml’ para definir o novo formulário e o controlador. Nós definimos os objetos que inserem as propriedaders para commandClass e validator. Também especificamos duas visões, um formView que é usado para o formulário e um successView que será exibido após o sucesso do processamento. O último pode ser de dois tipos. Pode ser uma referência normal a uma visão que é direcionado a uma de nossas páginas JSP. Uma desvantagem dessa abordagem é, que se o usuário recarregar a página, os dados do formulário serão submetidos novamente, e você acabará com um aumento duplicado. Uma maneira alternativa é usar um redirecionamento, onde a resposta é enviada de volta aos usuários instruindo o navegador a irem para uma nova URL. A URL que usaremos nesse caso não pode ser uma de nossas páginas JSP, já que elas estão ocultas a acesso direto. Tem que ser uma URL que seja alcançável externamente. Temos que que escolher ‘hello.htm’ como a URL de direcionamente. Essa URL é mapeada para a página ‘hello.jsp’, de forma que deve funcionar bem.

'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 -->

<beans>

    <bean id="productManager">
        <property name="products">
            <list>
                <ref bean="product1"/>
                <ref bean="product2"/>
                <ref bean="product3"/>
            </list>
        </property>
    </bean>

    <bean id="product1">
        <property name="description" value="Lamp"/>
        <property name="price" value="5.75"/>
    </bean>

    <bean id="product2">
        <property name="description" value="Table"/>
        <property name="price" value="75.25"/>
    </bean>

    <bean id="product3">
        <property name="description" value="Chair"/>
        <property name="price" value="22.79"/>
    </bean>

    <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 name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

Em seguida, vamos dar uma olhada no controlador desse formulário. O método onSubmit(..) assumi o controle e gera algumas informações de log antes de chamar o método increasePrice(..) do objeto ProductManager. Depois retorna um ModelAndView passando uma nova instância de um RedirectView criado usando a URL que informa o sucesso da operação.

'springapp/src/web/PriceIncreaseFormController.java':

package springapp.web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.PriceIncrease;

public class PriceIncreaseFormController extends SimpleFormController {

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    private ProductManager productManager;

    public ModelAndView onSubmit(Object command)
            throws ServletException {

        int increase = ((PriceIncrease) command).getPercentage();
        logger.info("Increasing prices by " + increase + "%.");

        productManager.increasePrice(increase);

        logger.info("returning from PriceIncreaseForm view to " + getSuccessView());

        return new ModelAndView(new RedirectView(getSuccessView()));
    }

    protected Object formBackingObject(HttpServletRequest request) throws ServletException {
        PriceIncrease priceIncrease = new PriceIncrease();
        priceIncrease.setPercentage(20);
        return priceIncrease;
    }

    public void setProductManager(ProductManager productManager) {
        this.productManager = productManager;
    }

    public ProductManager getProductManager() {
        return productManager;
    }

}

Também devemos adicionar algumas mensagens ao arquivo ‘messages.properties’.

'springapp/war/WEB-INF/classes/messages.properties':

title=SpringApp
heading=Hello :: SpringApp
greeting=Greetings, it is now
priceincrease.heading=Price Increase :: SpringApp
error.not-specified=Percentage not specified!!!
error.too-low=You have to specify a percentage higher than {0}!
error.too-high=Don''t be greedy - you can''t raise prices by more than {0}%!
required=Entry required.
typeMismatch=Invalid data.
typeMismatch.percentage=That is not a number!!!

Compile e carregue tudo isso e depois recarregue a aplicação para testa-la. Devem ser exibidas algumas mensagens de erro. Finalmente, adicionaremos um link para a página de aumento do preço no ‘hello.jsp’.

<%@ include file="/WEB-INF/jsp/include.jsp" %>

<html>
  <head><title><fmt:message key="title"/></title></head>
  <body>
    <h1><fmt:message key="heading"/></h1>
    <p><fmt:message key="greeting"/> <c:out value="${model.now}"/></p>
    <h3>Products</h3>
    <c:forEach items="${model.products}" var="prod">
      <c:out value="${prod.description}"/> <i>$<c:out value="${prod.price}"/></i><br><br>
    </c:forEach>
    <br>
    <a href="<c:url value="priceincrease.htm"/>">Increase Prices</a>
    <br>
  </body>
</html>

Agora, execute os alvos ‘deploy’ e ‘reload’ do Ant e teste a nossa funcionalidade de aumento de preço.

4.7. Sumário

Vamos dar uma olhada no que fizemos nessa Parte 4.

  1. Renomeamos nosso controlador para InventoryController e demos a ele uma referência a ProductManager para que possamos recuperar uma lista de produtos para exibição.
  2. Em seguida modificamos a página JSP para usar um empacotador de mensagem para texto estático e também adicionamos um loop forEach para exibir a lista de produtos.
  3. Depois definimos alguns dados de teste para popular os objetos do negócio.
  4. Depois disso tudo estar funcionando criamos um formulário para fornecer a habilidade de aumentas os preços.
  5. Finalmente criamos o controlador do formulário e um validador e carregamos a aplicação e testamos os novos recursos.

Abaixo segue uma tela de como deve ficar a estrutura de diretório de nosso projeto depois das instruções acima.