quinta-feira, 15 de setembro de 2011

Spring MVC e jQuery, simples e rápido

O objetivo é criar uma aplicação web tão simples e rápida quanto possível, assim iremos mostrar como desenvolver uma pequena aplicação web, usando Spring MVC como backend, jQuery como biblioteca javascript/ajax e  retornar alguns dados no formato JSON, que o Spring já tem suporte, usando a biblioteca Jackson JSON.

Overview

Nossa aplicação terá um Home e um pequeno Cadastro de Item, que deixaremos os dados em memória. Utilizaremos JSPs para as views e algumas classes Java pra serem controllers e services, resultando em um fluxo:

Requisição > Controller > Service, View

- Uma requisição é enviada.
- O Controller recebe.
- O Controller opcionalmente chama um Service para executar alguma lógica de negócio.
- O Controller cria um ModelAndView e coloca os dados necessários da view dentro do Model.
- O Controller dispacha para alguma view JSP usando JSTL ou retorna dados JSON.
- A JSP mostra as informações.

Home:



Listagem de Item:



Formulário de Item:




Configuração

Primeiro, precisaremos configurar o Spring MVC para receber nossas requisições. Inclua a servlet DispatcherServlet dentro do web.xml:

web.xml

    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:META-INF/applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

Depois, crie o arquivo applicationContext.xml dentro da pasta META-INF do seu source, conforme a configuração do valor do parametro contextConfigLocation do web.xml:

Aqui colocamos o mapeamento da servlet para /app/*, ou seja, todas as URLs com esse prefixo, serão direcionadas para a servlet do Spring. Outras URLs como /js, /css, /img, não serão direcionadas.

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: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.xsd">

    <mvc:annotation-driven />
   
    <context:component-scan base-package="com.naskar.springmvc"/>
   
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
      <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
      <property name="prefix" value="/WEB-INF/jsp/"/>
      <property name="suffix" value=".jsp"/>
      </bean>
 
</beans>

A configuração do parametro context:component-scan base-package diz ao Spring para procurar por todas as classes anotadas com @Controller e @Service dentro dos pacotes e subpacotes com.naskar.springmvc.

Depois disto, temos a configuração do InternalResourceViewResolver, que estamos direcionando para um diretório dentro de WEB-INF, assim as JSPs não estarão disponíveis diretamente para o usuário e todas as requisições terão sempre que passar por algum Controller para serem exibidas.

Se você estiver usando maven, inclua as seguintes dependências no seu arquivo pom.xml:

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

        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-asl</artifactId>
            <version>1.8.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>codehaus-snapshots</id>
            <url>http://snapshots.repository.codehaus.org</url>
        </repository>
    </repositories>

Caso não, veja as referências abaixo para efetuar o download das bibliotecas.
Jackson será necessário somente se você quiser usar JSON.


Home

Agora vamos criar o HomeController, que irá receber as requisições para as URLs do tipo /app/home:

HomeController.java

@Controller
public class HomeController {
   
    @RequestMapping("/home")
    public ModelAndView home() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("home");
        return mav;
    }
   
    @RequestMapping("/home/welcome")
    public ModelAndView welcome() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("welcome");
       
        mav.addObject("message", "Bem vindo!!!");
       
        return mav;
    }

}

Para todas as classes que iremos usar como controllers, precisamos anotá-las com @Controller, e para cada URL que queremos disponibilizar, utilizaremos a anotação @RequestMapping, para fazer o mapeamento entre o path e o método a ser executado. Assim, temos:

/app/home
Será executado o método home() e dispachada a requisição para a página WEB-INF/jsp/home.jsp.

/app/home/welcome
Será executado o método welcome() e dispachada a requisição para a página WEB-INF/jsp/welcome.jsp.

home.jsp:

<%@page deferredSyntaxAllowedAsLiteral="true"%>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
        <link type="text/css" href="/springmvc/css/pepper-grinder/jquery-ui-1.8.16.custom.css" rel="stylesheet" />   
        <script type="text/javascript" src="/springmvc/js/jquery-1.6.2.min.js"></script>
        <script type="text/javascript" src="/springmvc/js/jquery-ui-1.8.16.custom.min.js"></script>
        <script>
            $(function() {
                   
                // add the itens to content
                $("#HomeBtn").button().click(function() {
                    $("#content").load("home/welcome");
                });
               
                // add the itens to content
                $("#itemBtn").button().click(function() {
                    $("#content").load("item");
                });
               
               
                // init
                $("#content").load("home/welcome");
               
            });   
            </script>
    </head>
    <body>
        <div id="menu" class="ui-widget">
            <div>
                <input type="button" id="HomeBtn" value="Home" />
                <input type="button" id="itemBtn" value="Item" />
            </div>
        </div>
        <div id="content" style="margin: 10px;">
        </div>
    </body>
</html>

Podemos vê que a página é simples e clara. Usaremos jQuery para carregar as outras páginas dentro do DIV content, usando a seguinte função load:

$("#content").load("item");

Isso faz com o jQuery procure algum element com o id content, que no nosso caso é um elemento DIV, e carregue o conteúdo da URL /app/item dentro do DIV.

Logo acima, temos a adição dos eventos click() dos botões para executar os carregamentos das páginas.


Dados para view

Um detalhe que precisamos perceber é que podemos passar dados para a view, utilizando o método addObject da classe ModelAndView, como é feito para a view welcome.

Logo que a página é carregada, é executado um load para a URL welcome, que mostrará uma mensagem "Bem vindo!!!", essa incluída durante a execução do HomeController:

welcome.jsp

<div id="welcome_conteudo">

    <div id="welcome_conteudo" class="ui-dialog ui-widget ui-widget-content ui-corner-all undefined ui-resizable">
       <div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix">
          <span id="ui-dialog-title-dialog" class="ui-dialog-title">Home</span>
       </div>
       <div style="height: 200px; width: auto;" class="ui-dialog-content ui-widget-content" id="dialog">
          <p>${message}</p>
       </div>
    </div>
   
</div>

Podemos perceber também, que dentro da JSP welcome.jsp só é necessário um framento de página que será incluído dentro do DIV content.


Item

Agora veremos o cadastro de item, que utiliza uma classe ItemService para armazenar os Items em memória que serão cadastrados.

ItemController.java

@Controller
public class ItemController {
   
    @Autowired
    private ItemService itemService;
   
    @RequestMapping("/item")
    public ModelAndView list() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("item/list");
       
        mav.addObject("items", itemService.getItems());
       
        return mav;
    }
   
    @RequestMapping("/item/add")
    public ModelAndView add() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("item/form");
        return mav;
    }
...
}

ItemServiceImpl.java

@Service
public class ItemServiceImpl implements ItemService {
   
    private List<Item> items;
   
    public ItemServiceImpl() {
        items = new ArrayList<Item>();
    }
   
    public List<Item> getItems() {
        return items;
    }
   
    public void save(Item item) {
        if(items.contains(item)) {
            items.remove(item);
        }
        items.add(item);
    }
   
    public void remove(Item item) {
        items.remove(item);
    }
   
    public Item load(Integer itemId) {
        int i = items.indexOf(new Item(itemId, ""));
        if(i > -1)
            return items.get(i);
        else
            return null;
    }

}

Podemos vê que ao tentarmos acessar a URL /app/item, temos um mapeamento para o métod list(), que executará o método getItems() da classe ItemService.

Depois disso, o ItemController, adiciona ao ModelAndView uma propriedade items com a lista de itens e dispacha para a view item/list. O viewResolver, que vimos no applicationContext, irá dispachar para a JSP /WEB-INF/jsp/item/list.jsp, que mostrará os dados.

list.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<div id="item_content">
    <script type="text/javascript">
        $(function(){
           
            $("#item_add")
                .click(function() {
                    $('#item_content').load("item/add");
                });
               
        });
   
        function loadAction(action) {
            $('#item_content').load(action);
        }
   
    </script>
   
    <div class="ui-dialog ui-widget ui-widget-content ui-corner-all undefined ui-resizable">
       <div class="ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix">
          <span id="ui-dialog-title-dialog" class="ui-dialog-title">Item</span>
       </div>
       <div style="height: 200px; width: auto;" class="ui-dialog-content ui-widget-content" id="dialog">
      
               <div>
                <c:forEach var="message" items="${messages}">
                ${message}<br />
                </c:forEach>
                <br />
            </div>
         
              <div>
                <a id="item_add" href="#">Novo</a>
            </div>
            <div>
                <table>
                    <thead>
                        <tr>
                            <td>
                                ID
                            </td>
                            <td>
                                Descricao
                            </td>
                        </tr>
                    </thead>
                    <tbody>
                        <c:forEach var="item" items="${items}">
                        <tr>
                            <td>
                                ${item.id}
                            </td>
                            <td>
                                ${item.description}
                            </td>
                            <td>
                                <a id="item_edit" href="javascript:loadAction('item/edit/${item.id}');">#editar</a>
                            </td>
                            <td>
                                <a id="item_remove" href="javascript:loadAction('item/remove/${item.id}');">#excluir</a>
                            </td>
                        </tr>
                        </c:forEach>
                    </tbody>
                </table>
            </div>
         
         
       </div>
    </div>

</div>


Novamente, temos somente um pequeno framento de código que será carregado dentro do DIV content.

Aqui também, já podemos vê a utilização de uma URL para efetuar a exclusão de um Item:

/app/item/remove/${item.id}

Para efetuar o cadastro das informações temos o seguintes mapeamentos:

@RequestMapping("/item/edit/{itemId}")
    public ModelAndView editar(@PathVariable Integer itemId) {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("item/form");
      
        mav.addObject("item", itemService.load(itemId));
      
        return mav;
    }
   
    @RequestMapping("/item/remove/{itemId}")
    public ModelAndView remove(@PathVariable Integer itemId) {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("forward:/app/item");
      
        itemService.remove(new Item(itemId, ""));
       
        List<String> messages = new ArrayList<String>();
        messages.add("Item removido com sucesso.");
       
        mav.addObject("item", itemService.getItems());
        mav.addObject("messages", messages);
      
        return mav;
    }
   
    @RequestMapping(value = "/item/save")
    public ModelAndView save(Item item) {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("forward:");
      
        itemService.save(item);
       
        List<String> messages = new ArrayList<String>();
        messages.add("Item salvo com sucesso.");
       
        mav.addObject("item", itemService.getItems());
        mav.addObject("messages", messages);
      
        return mav;
    }

A diferença aqui está somente na utilização da anotação @PathVariable, que mapea um parametro do método para a URL.

@RequestMapping("/item/remove/{itemId}")
public ModelAndView remove(@PathVariable Integer itemId)

Outra característica interessante do Spring MVC é utilizar a String "forward:" para fazer um redirecionamento utilizando todos os dados do model anterior.

mav.setViewName("forward:/app/item");


JSON

Para finalizar, veremos agora como retornar alguns dados no formato JSON, que utilizando Spring MVC é realmente muito simples. Incluiremos mais esse mapeamento dentro da classe ItemController:

@RequestMapping(value = "/item/list.json")
    public @ResponseBody List<Item> listjson() {
        return itemService.getItems();
    }

A anotação @ResponseBody fará com que, caso a biblioteca Jackson esteja no classpath, o Spring converta o List<Item> para o formato JSON. Para fazermos o teste podemos utilizar o navegador ou uma ferramenta chamada curl:

Se fizermos uma requisição para a URL:

/app/item/list.json

Teremos o seguinte retorno:

[{"id":1,"description":"item1"},{"id":2,"description":"item2"}]

Aqui vemos o log do curl:

curl -v -i -H "Accept: application/json" -X POST -d "" "http://localhost:8080/springmvc/app/item/list.json"

* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /springmvc/app/item/list.json HTTP/1.1
> User-Agent: curl/7.19.3
> Host: localhost:8080
> Accept: application/json
> Content-Length: 0
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Content-Type: application/json
Content-Type: application/json
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Server: Jetty(8.0.1.v20110908)
Server: Jetty(8.0.1.v20110908)

<
[{"id":1,"description":"item1"},{"id":2,"description":"item2"}]* Connection #0 t
o host localhost left intact
* Closing connection #0

Para deixarmos a aplicação mais exuta, podemos ao invés de retornarmos framentos de página usando JSP, podemos retornar somente JSON, e fazermos todo o tratamento no cliente usando Javascript e componentes como datatables, calendars, etc, auxiliados pela função $.getJSON() do jQuery.


Considerações


Arquiteturas exutas

Aplicações para web, onde há uma grande necessidade de performance, podem se beneficiar de arquiteturas mais exutas, evitado o uso de frameworks pesados como JSF, na qual são baseados em componentes. Aqui, mostramos um modelo baseado em actions.

Largura de banda

O carregamento dos dados sob demanda e em pequenos framentos também economiza uma grande largura de banda, como também de processo de renderização da página.

Usabilidade

Outra questão importante, é a usabilidade da aplicação que aumenta de forma consideralmente, já que com a nova versão do HTML versão 5, temos mais opções disponíveis e podemos considerar o descarte de frameworks de componentes java.

Cases

Há vários casos de utilização desse modelo em sites com um número grande de acessos. Um exemplo clássico é o facebook que utiliza PHP como backend e javascript intensamente, e até desenvolveu um compilador para PHP, para suportar a quantidade enorme de acessos.

Simplificação do protocolo

Nessa abordagem, temos também o ganho da abstração do backend utilizado, já que, o que são enviados para o servidor, são somente GET/POST ou dados no formato JSON, que é mais exuto que a utilização de XML com SOAP, podendo ser uma ótíma opção para exposição de webservices.

Dados de Sessão

A maioria das aplicações usam cookies para armazenar dados de sessão no backend. Uma boa opção é usar banco de dados não estruturados ou orientados a documentos, onde eles só armazenam dados na forma de chave/valor, com uma alta performance de replicação e disponibilidade, exemplos são CouchDB, MongoDB.


Conclusão

Por mais que JSF seja o padrão JEE, não quer dizer que precisamos usá-lo sempre. A arquitetura de um sistema depende de três fatores principais: atributos de qualidade (escalabilidade, performance, etc..), ambiente (experiência da equipe, ferramentas, etc) e experiência do arquiteto. Assim, nós como arquitetos, temos que nos abastecermos da maior quantidade de experiências para escolhermos a melhor solução para cada caso, independente da tecnologia ou plataforma, seja Java, PHP ou .NET.


Referências

jQuery e Spring MVC: http://blog.springsource.com/2010/01/25/ajax-simplifications-in-spring-3-0/
Jackson: http://jackson.codehaus.org/
CounchDB: http://couchdb.apache.org/
MongoDB: http://www.mongodb.org/
cURL: http://curl.haxx.se/download.html

Os fontes desse exemplo estão disponiveis no googlecode:

http://code.google.com/p/com-naskar-springmvc/downloads/list
http://code.google.com/p/com-naskar-springmvc/source/browse/