GWT + Grails = Rock n Roll: Parte 2 - Separados e falando com RequestBuiler e JSON

Este post é a Parte 2 da sequência de posts sobre o tema GWT + Grails = Rock n Roll. Você pode ver a Parte 1 aqui. Nesta primeira mostrei como criar uma aplicação baseada em GWT e Grails. Para juntar os dois, usei o plugin Grails-Gwt.

Neste post de hoje, depois de alguns dias lendo sobre GWT, fazendo testes e brincando com a aplicação criada no post da Parte 1, resolvi tentar também desenvolver outra aplicação com GWT + Grails mas desta vez sem usar o plugin. Desta forma, poderia eu mesmo decidir se prefiro com ou sem o Plugin. Este post, então, mostrará como criar uma solução composta de duas aplicações isoladas, uma GWT e outra Grails, que se falam via troca de dados usando JSON.

Separando Grails e GWT em duas aplicações distintas

Como descrevi na Parte 1, não usar o plugin e ter duas aplicações distintas (uma GWT e outra Grails) significa ter que lidar com a Same Origin Policy (SOP) em tempo de desenvolvimento. Em produção simplesmente pode-se dar um jeito de levar os arquivos Html, Css e Javascript gerados pelo GWT para o propeto grails (em último caso, basta copiar e colar de um lado para o outro). Mas em desenvolvimento é preciso lidar com SOP de uma maneira mais produtiva do que ter que ficar copiando e colando de um lado para outro. A maneira mais simples que encontrei foi usando uma ProxyServlet, originalmente criada por Jason Edward, e depois evoluída por Matt Raible. O código fonte desta classe eu peguei no projeto Grails OAuth Plugin criado pelo M Raible. É simples de usar, e resolve o problema facilmente. O que ela faz neste nosso contexto é:

  • a aplicações GWT no cliente faz uma requisição para o servidor;
  • a aplicações GWT no servidor recebe esta requisição e repassa para a aplicação Grails
  • a aplicação Grails recebe esta requisição e responde JSON
  • a aplicação GWT recebe esta resposta JSON e repassa para o cliente GWT

Desta forma não precisamos nos preocupar com SOP.

O que se ganha em fazer isso? Para que, afinal, quero ter duas aplicações separadas (uma GWT e outra Grails). Bom, isso acaba sendo uma decisão pessoal de cada um, ok? Mas para mim, comecei a achar mais interessante seguir os padrões de cada projeto. Um projeto GWT, segundo as recomendações, deve ser organizado de uma maneira, com determinada árvore de diretórios, etc. Pode não ser seguido, mas é um padrão recomendado. Então, porque não seguir e falar a mesma lingua em chats, fóruns de discussão, novos desenvolvedores na equipe, etc?

Além disso, separar os dois projetos provoca uma reflexão conceitual interessante sobre arquitetura de aplicações ricas para a web (RIA - Rich Internet Applications). Ao separar, você está literalmente, fisicamente, implementando a arquitetura SOFEA (Service Oriented Front-End Architecture). SOFEA foi muito bem apresentado no artigo Life Above the Service Tier (vale a leitura. Este pessoa hoje em dia criou um grupo sobre este assunto e tem um site em: ThinServerArchitecture.com ).

Numa arquiteturea SOFEA, toda a lógica de interface com usuário está sendo rodada no cliente, no nosso caso, no browser. Aqui então, as camadas View e Contoller da aplicação terão um projeto só pra elas (o projeto GWT). Ou seja, o desacoplamento é total. Você simplesmente desacopla o máximo que pode o tal do Front-End. Isso é interessante na minha opinião, pois aí a sua aplicação servidora, que roda no servidor, ela acaba por ter a responsabilidade apenas de prover serviços para a aplicação de Front-End (User Interface - UI). Parece até que estamos voltando para o conceito Cliente x Servidor, e de certa forma, estamos sim, mas um pouco diferente, pois é um Thin Client, onde apenas as regras de interface e controle de fluxo de navegação estão no cliente. Todas as demais regras de negócio, estão e devem estar no servidor.

Outras duas justificativas para esta separação em duas aplicações são:
  • Pode ficar mais simples equipes diferentes trabalharem cada uma em um projeto diferente
  • Cada projeto pode ser versionado independentemente
Bom, depois de todas essas justificativas, o fato é: criei uma nova aplicação que é composta por um projeto GWT responsável pelo Front-End, e por outro projeto Grails responsável pelo Back-End (provendo serviços, lembra SOA?). Veja que as coisas começam até a ficar mais coesas e desacopladas. Interessante resultado....

Comunicação entre GWT e Grails com JSON

Para a comunicação entre o Front-End e o Back-End, escolhi usar JSON. A aplicação grails pode facilmente gerar uma resposta JSON, enquanto que a aplicação GWT pode facilmente consumir JSON. Então, sendo o JSON um formato leve para troca de dados, e sendo ele facilmente servido e consumido pelo Grails e GWT respectivamente, acho que JSON é uma boa escolha. Mas, se você preferir trabalhar com XML, ou outro formato proprietário, a decisão é sua.

Portanto, para criarmos esta nova aplicação, basta criar um novo projeto GWT, e outro Grails.

Aplicação Front-End (GWT)

Eu estou usando o Eclipse com o plugin do GWT, portanto, para criar um novo projeto basta clicar na opção de criar novo projeto GWT do Eclipse. Criei este projeto GWT chamado rockgwtfront, com o pacote com.rockgwt.front

O wizard do Eclipse já cria uma classe EntryPoint, o arquivo XML de configuração do Module, e também cria um Serviço RPC de exemplo. Mas não precisaremos deste serviço para este exemplo, portanto, pode apagar o pacote com.rockgwt.front.server e o pacote com.rockgwt.front.shared, e também pode apagar os arquivos GreetingService.java e GreetingServiceAsync.java do pacote com.rockgwt.front.client. Depois altere a classe Rockgwtfront.java para não usar mais este Serviço RPC. No final das contas, fiquei com a classe de EntryPoint abaixo:

package com.rockgwt.front.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;

public class Rockgwtfront implements EntryPoint {

    public void onModuleLoad() {
        final Button sendButton = new Button("Get Hendrix songs");
        final FlexTable songsTable = new FlexTable();

        // We can add style names to widgets
        sendButton.addStyleName("sendButton");
        
        sendButton.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                String hendrixSongsServiceUrl = GWT.getHostPageBaseURL();
                if(!GWT.getHostPageBaseURL().contains("rockgwt"))
                    hendrixSongsServiceUrl += "rockgwt/";
                    
                hendrixSongsServiceUrl += "hendrix/songs";
                RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, hendrixSongsServiceUrl);
                builder.setTimeoutMillis(10000);
                builder.setCallback(new RequestCallback() {
                    
                    @Override
                    public void onResponseReceived(Request request, Response response) {
                        if (response.getStatusCode() == 200) {
                            
                            JSONValue v = JSONParser.parse(response.getText());
                            JSONArray songs = v.isArray();
                            if(songs != null){
                                updateBacklogItemsTable(songsTable, songs);
                            }
                            else{
                                throw new JSONException("O retorno nao veio como um objeto de Projeto"); 
                            }
                        } else {
                            onError(request, new RequestException(response.getText()));
                        }
                    }
                    
                    @Override
                    public void onError(Request request, Throwable exception) {
                        Window.alert(exception.getLocalizedMessage());
                    }
                });
                try {
                    builder.send();
                } catch (RequestException e) {
                    Window.alert(e.getLocalizedMessage());
                }
            }
        });

        RootPanel.get("sendButtonContainer").add(sendButton);
        RootPanel.get("songsTableContainer").add(songsTable);

    }
    
    private void updateBacklogItemsTable(FlexTable songsTable, JSONArray songs) {
        songsTable.clear();
        for (int i=0; i<songs.size(); i++) {
            JSONObject item = songs.get(i).isObject();
            songsTable.setWidget(i, 0, new Label(item.get("name").isString().stringValue()));
            songsTable.setWidget(i, 1, new Label(item.get("album").isString().stringValue()));
        }
                    
    }
}


Veja que a classe acima utiliza um RequestBuilder para realizar a requisição no servidor. Este RequestBuilder é uma classe do GWT que pode ser usada para fazer requisições remotas. Com ela, as requisições são sempre assíncronas, SEMPRE. Veja que passei um objeto Callback que será chamado quando esta requisição voltar do servidor. Assim, neste callback posso fazer o que for preciso com a resposta recebida. Neste caso, devo fazer o parse do JSON para poder trabalhar com os dados retornados. No fim de tudo, pego esses dados e os apresento numa tabela, só isso.

Veja que a aplicação GWT espera um retorno JSON no seguinte formato:
[{"name":"Hey Joe", "album":"Are You Experienced"},
{"name":"Bold as Love", "album":"Axis: Bold as Love"}]

Portanto, é preciso que a aplicação Grails retorne um JSON exatamente neste formato acima.

É preciso adicionar ao ~/rockgwtfront/war/WEB-INF/lib  do projeto os JARs necessários para a ProxyServlet citada acima. Portanto, adicione commons-fileupload-1.2.1.jar, httpclient-4.0.1.jar, e httpmime-4.0.1.jar ao lib. Crie a classe ProxyServlet. Você pode baixar o OAuth 1.3 e pegar o ProxyServlet de lá. Em seguida, é preciso configurar o web.xml para que ele saiba que o ProxyServlet existe. Vou mapear este proxyServlet para ele ser chamado para toda requisição que tenha o padrão /rockgwtback/*. Veja abaixo:


<web-app>
  <servlet>
        <servlet-name>RockServletservlet-name>
        <servlet-class>com.rockgwt.front.servlet.ProxyServletservlet-class>
        <init-param>
            <param-name>proxyHostparam-name>
            <param-value>localhostparam-value>
        init-param>
        <init-param>
            <param-name>proxyPortparam-name>
            <param-value>8080param-value>
        init-param>
        <init-param>
            <param-name>secureparam-name>
            <param-value>falseparam-value>
        init-param>
    servlet>
  
    <servlet-mapping>
        <servlet-name>RockServletservlet-name>
        <url-pattern>/rockgwt/*url-pattern>
    servlet-mapping>
  
 
  <welcome-file-list>
    <welcome-file>Rockgwtfront.htmlwelcome-file>
  welcome-file-list>
web-app>

Veja que o servlet-mapping mostra como deverá ser a url a ser chamada pelo cliente GWT para o servidor Grails. ( /rockgwtback/* ).
Aplicação Back-End (Grails)

Para o projeto Grails, ou você cria normalmente por linha de comando, ou pode até criar pelo Eclipse mesmo se estiver usando o STS (SpringSource Tool Suite). Criei este projeto Grails chamando-o de rockgwtback. Para este projeto, criei um controller chamado HendrixController que possui a action "musics". Esta action retorna uma lista de músicas do Hendrix. Veja abaixo:

package com.rockgwt.back.controller

class HendrixController {
    def index = { }

    def songs = {
        println "chegou"
        render(builder: 'json') {
                songs = array {
                       song name: '1) Hey Joe', album: 'Are You Experienced'
                       song name: '2) Bold as Love', album: 'Axis: Bold as Love'
                   }
        }
    }

}

Alterei também o nome da aplicação dentro do application.properties, colocando app.name=rockgwt, pois foi assim que eu coloquei a URL da requisição lá em cima, na aplicação GWT, e também no servlet-mappging do web.xml

É isso!

Assim, temos um sistema rodando com Grails e GWT, independentes um do outro, mas juntos formando a nossa aplicação. Faltaria aqui neste exemplo uma forma de juntar as duas aplicações para produção. Provavelmente faria isso com o Maven. Em produção, então, seria uma única aplicação mesmo.

1 comentários:

Gregory Fontenele 23 de março de 2010 às 10:16

Muito bom, bastante interessante esse assunto