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