Armazenamento do GAE com Bigtable, Blobstore e Google Storage

Aprenda da sua própria maneira três opções de armazenamento de dados para o GAE

O Google App Engine evita o banco de dados relacional em favor de vários armazenamentos de dados não relacionais: Bigtable, Blobstore e o mais recente no pedaço, o Google Storage for Developers. O autor John Wheeler explora os prós e os contras das três opções de armazenamento de muitos dados do GAE, guiando-o ao mesmo tempo por um cenário de aplicativo que o familiarizará com a configuração e o uso de cada uma.

John Wheeler, Applications manager, Xerox

John Wheeler has been programming professionally for over a decade. He is the co-author of Spring in Practice and works for Xerox as an applications manager. Visit John's website for more of his writing about software development.



22/Jul/2011

Como eles estão sempre por perto para descarregarmos bytes neles, é fácil aceitar as unidades de disco e os sistemas de arquivos neles como uma coisa natural. Quando você está escrevendo um arquivo, não precisa se preocupar com muito mais além de sua localização, permissões e requisitos de espaço. Basta construir um java.io.File e começar a trabalhar; ojava.io.File funciona do mesmo jeito, seja em um computador desktop, servidor da Web ou dispositivo remoto. Mas quando você começa a trabalhar com o Google App Engine (GAE), essa transparência, ou a falta dela, torna-se aparente muito rápido. No GAE, não é possível gravar arquivos em disco, já que não há sistema de arquivos utilizável. Na verdade, somente declarar um java.io.FileInputStream geraria um erro de compilação, porque essa classe foi incluída na lista de bloqueio a partir do GAE SDK.

Felizmente, a vida oferece opções, e o GAE oferece algumas opções particularmente eficientes para armazenamento. Como ele foi elaborado a partir do zero tendo a escalabilidade como objetivo, o GAE oferece dois armazenamentos de valor da chave: o Datastore (também conhecido como Bigtable) retém os dados normais que você normalmente jogaria em um banco de dados, enquanto o Blobstore mantém blobs binários enormes. Os dois possuem acesso de tempo constante e são completamente diferentes dos sistemas de arquivos com os quais você já trabalhou.

Além destes dois, há um recém-chegado na turma: o Google Storage for Developers. Ele trabalha como o Amazon S3, que também é bastante diferente de um sistema de arquivos tradicional. Neste artigo, construiremos um aplicativo de exemplo que implementa cada opção de armazenamento do GAE, um por um. Você irá adquirir uma experiência prática usando Bigtable, Blobstore e Google Storage for Developers, e entender os prós e contras de cada implementação.

Você precisará de:

Você precisará de uma conta do GAE e diversas ferramentas grátis de software livre para trabalhar nos exemplos deste artigo. Para o seu ambiente de desenvolvimento, você precisará de JDK 5 ou JDK 6 e Eclipse IDE for Java™ developers. Você também precisará de:

O Google Storage for developers está disponível atualmente somente para um número limitado de desenvolvedores nos Estados Unidos. Se não conseguir obter acesso ao Google Storage imediatamente, você pode seguir os exemplos para Bigtable e Blobstore e obter uma boa noção de como o Google Storage funciona.

Configuração preliminar: O aplicativo de exemplo

Antes de começarmos a explorar os sistemas de armazenamento do GAE, precisamos criar as três classes necessárias para nosso aplicativo de exemplo:

  • Um bean que represente uma fotografia. Photo contém campos como título e legenda, além de outros para armazenar dados de imagem binária.
  • Um DAO que persista Photos no armazenamento de dados do GAE, também conhecido como Bigtable. O DAO contém um método para inserção de Photos e outro para extraí-los por meio de ID. Ele usa uma biblioteca de software livre chamada Objectify-Appengine para persistência.
  • Um servlet que use o padrão Template Method para sintetizar um fluxo de trabalho de três etapas. Usaremos o fluxo de trabalho para explorar cada opção de armazenamento do GAE.

Fluxo de trabalho do aplicativo

Seguiremos o mesmo procedimento para cada uma das opções de armazenamento de dados do GAE; isso lhe dará a oportunidade de focar a tecnologia, e também comparar os prós e os contras de cada método de armazenamento.O fluxo de trabalho do aplicativo será o mesmo todas as vezes:

  1. Exibir e fazer upload do formulário.
  2. Fazer upload de uma imagem para armazenamento e salvar um registro no armazenamento de dados.
  3. Apresentar a imagem.

A figura 1 é um diagrama do fluxo de trabalho do aplicativo:

Figura 1. O fluxo de trabalho de três etapas usado para demonstrar cada opção de armazenamento
O fluxo de trabalho de três etapas usado para demonstrar cada opção de armazenamento

Como benefício adicional, o aplicativo de exemplo também permite que você pratique tarefas que são essenciais para qualquer projeto do GAE que grave e forneça binários. Agora, vamos começar a criar essas classes!


Um aplicativo simples para GAE

Faça o download do Eclipse se não o tiver e depois instale o plug-in do Google para Eclipse e crie um novo projeto do Google Web Application que não use GWT. Consulte o código de amostra incluído neste artigo para obter orientação sobre arquivos de estruturação de projeto. Assim que tiver configurado seu Web app do Google, inclua a primeira classe do aplicativo, Photo, como mostrado na listagem 1. (Observe que omiti os getters e setters.)

Listagem 1. Photo
import javax.persistence.Id;

public class Photo {

    @Id
    private Long id;
    private String title;
    private String caption;
    private String contentType;
    private byte[] photoData;
    private String photoPath;

    public Photo() {
    }

    public Photo(String title, String caption) {
        this.title = title;
        this.caption = caption;
    }

    // getters and setters omitted
}

A anotação @Id designa qual campo é uma chave primária, o que será importante quando começarmos a trabalhos com o Objectify. Cada registro salvo no armazenamento de dados, também chamado de entidade, requer uma chave primária. Quando uma imagem é transferida por upload, uma opção é armazená-la diretamente em photoData, que é uma array de bytes. Ela é gravada no armazenamento de dados como propriedadeBlob , juntamente com o restante dos campos de Photo. Em outras palavras, a imagem é salva e trazida junto ao bean. Se, em vez disso, uma imagem for transferida por upload para o Blobstore ou o Google Storage, os dados são armazenados externamente nesse sistema e photoPath aponta sua localização. Apenas photoData ouphotoPath é usada em qualquer um dos casos. A figura 2 esclarece a função de cada uma:

Figure 2. Como photoData e photoPath funcionam
Figure 2. Como photoData e photoPath funcionam

A seguir, abordaremos a persistência do bean.

Persistência baseada em objeto

Como mencionado anteriormente, usaremos Objectify para criar uma DAO para o bean de Photo .Embora JDO e JPA possam ser APIs de persistência mais populares e comuns, elas possuem curvas de aprendizado mais acentuadas. Outra opção seria usar a API de armazenamento de dados de baixo nível do GAE, mas isso envolve o tedioso trabalho de serializar os beans para e de entidades de armazenamento de dados. O Objectify toma conta disso, por meio de reflexo Java. (Consulte Recursos para saber mais sobre alternativas de persistência do GAE, incluindo Objectify-Appengine.)

Comece criando uma classe chamada PhotoDao e codificando-a como mostrado na listagem 2:

Listagem 2. PhotoDao
import com.googlecode.objectify.*;
import com.googlecode.objectify.helper.DAOBase;

public class PhotoDao extends DAOBase {

    static {
        ObjectifyService.register(Photo.class);
    }

    public Photo save(Photo photo) {
        ofy().put(photo);
        return photo;
    }
    
    public Photo findById(Long id) {
        Key<Photo> key = new Key<Photo>(Photo.class, id);
        return ofy().get(key);
    }
}

PhotoDao estende DAOBase, uma classe de conveniência que carrega lentamente uma instância Objectify . Objectify é nossa interface primária na API e é exposto através do método ofy . Antes de usarmos ofy, no entanto, precisamos registrar classes de persistência em um inicializador estático como Photo na listagem 2.

O DAO contém dois métodos para inserção e descoberta de Photos. Em cada um deles, trabalhar com o Objectify é tão simples quanto trabalhar com hashtable. Você deve notar quePhotos são trazidas com um Key em findById, mas não se preocupe com isso: para os fins deste artigo, pense em Key como um wrapper em torno do campo id .

Agora temos um bean de Photo e uma PhotoDao para gerenciar a persistência. A seguir, detalharemos o fluxo de trabalho do aplicativo.

Fluxo de trabalho do aplicativo, por meio do padrão Template Method

Se você já jogou Mad Libs, o padrão Template Method fará bastante sentido para você. Cada Mad Lib apresenta uma história com muitos trechos em branco para que os leitores os preencham. A entrada do leitor — como os trechos em branco são completados — altera a história drasticamente. De modo semelhante, as classes que usam o padrão Template Method contêm uma série de etapas, e algumas são deixadas em branco.

Construiremos um servlet que use o padrão Template Method para realizar o fluxo de trabalho do nosso aplicativo de exemplo. Comece separando um servlet abstrato, nomeando-o AbstractUploadServlet. É possível usar o código na listagem 3 como referência:

Listagem 3. AbstractUploadServlet
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public abstract class AbstractUploadServlet extends HttpServlet {

}

Em seguida, adicione os três métodos abstratos da listagem 4. Cada um representa uma etapa do fluxo de trabalho.

Listagem 4. Três métodos abstratos
protected abstract void showForm(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException;

protected abstract void handleSubmit(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException;

protected abstract void showRecord(long id, HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException;

Agora, já que estamos usando o padrão Template Method, pense nos métodos da listagem 4 como os trechos em branco, e no código da listagem 5 como a história que os une:

Listagem 5. O surgimento de um fluxo de trabalho
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    String action = req.getParameter("action");
    if ("display".equals(action)) {
        // don't know why GAE appends underscores to the query string
        long id = Long.parseLong(req.getParameter("id").replace("_", ""));
        showRecord(id, req, resp);
    } else {
        showForm(req, resp);
    }
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
    throws ServletException, IOException {
    handleSubmit(req, resp);
}

Um lembrete sobre servlets

Caso já faça algum tempo desde que você trabalhou pela última vez com servlets antigos simples, doGet e doPost são métodos padrão para manipular HTTP GETs e POSTs. É uma prática comum usar GET para buscar recurso da Web e POST para enviar dados. Sendo assim, nossa implementação de doGet exibe um formulário de upload ou uma foto do armazenamento, e doPost trata dos envios dos formulários para upload. Fica a cargo das classes que estendem AbstractUploadServlet definirem cada parte do comportamento. O diagrama na figura 3 mostra a sequência de eventos ocorridos. Pode levar alguns minutos para se ter uma visão clara do que está realmente acontecendo.

Figura 3. O fluxo de trabalho em um diagrama de sequência
O fluxo de trabalho em um diagrama de sequência

Com as três classes construídas, nosso aplicativo de exemplo está pronto para funcionar. Agora podemos focar como cada uma das opções de armazenamento do GAE interage com o fluxo de trabalho do aplicativo, começando com o Bigtable.


Opção nº 1 de armazenamento do GAE: Bigtable

A documentação do GAE do Google descreve o Bigtable como uma array fragmentada e classificada, mas acho mais fácil pensar nele como uma hashtable gigante repartida entre servidores enormes. Como um banco de dados relacional, o Bigtable possui tipos de dados. Na verdade, tanto o Bigtable quanto os bancos de dados relacionais usam o tipo blob para armazenar binários.

Não confunda o tipo blob com o Blobstore — que é outro armazenamento de valor da chave do GAE, e que exploraremos a seguir.

Trabalhar com blobs no Bigtable é mais prático porque eles estão carregados ao lado de outros campos, tornando-o disponíveis imediatamente. A única limitação é que os blobs não podem ser maiores que 1 MB, embora essa restrição deva ser relaxada no futuro. Seria bastante difícil encontrar uma câmera digital hoje em dia que tire fotos menores do que isso, de modo que usar o Bigtable pode representar um retrocesso para qualquer caso de uso que envolva imagens (como o nosso aplicativo de exemplo). Se a regra de 1 MB estiver bem para você, ou se você vai armazenar algo menor do que imagens, o Bigtable pode ser uma boa escolha: das três alternativas de armazenamento do GAE, ela é a mais fácil de se trabalhar.

Antes de fazer o upload de dados para o Bigtable, precisaremos criar um formulário de upload. Em seguida, trabalharemos na implementação de servlet, que consiste em três métodos abstratos customizados para o Bigtable. Finalmente, implementaremos a manipulação de erros, já que o limite de 1 MB é fácil de ser quebrado.

Crie o formulário de upload

A figura 4 mostra o formulário de upload do Bigtable:

Figura 4. Um formulário de upload do Bigtable
Um formulário de upload do Bigtable

Para criar este formulário, comece com um arquivo chamado datastore.jsp e depois insira o bloco de código da listagem 6:

Listagem 6. O formulário de upload customizado
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>		
        <form method="POST" enctype="multipart/form-data">
            <table>
                <tr>	
                    <td>Title</td>
                    <td><input type="text" name="title" /></td>
                </tr>
                <tr>	
                    <td>Caption</td>
                    <td><input type="text" name="caption" /></td>
                </tr>
                <tr>	
                    <td>Upload</td>
                    <td><input type="file" name="file" /></td>
                </tr>
                <tr>
                    <td colspan="2"><input type="submit" /></td>
                </tr>				
            </table>
        </form>
    </body>	
</html>

O formulário deve ter seu atributo de método definido como POST e um tipo incluído de dados de formulário/multipartes. Como nenhum atributo de action é especificado, o formulário é enviado para si mesmo. Usando POST, terminamos no AbstractUploadServlet's doPost, que, por sua vez, chama handleSubmit.

Temos o formulário, então vamos prosseguir com o servlet por trás dele.

Upload para e do Bigtable

Aqui, implementamos os três métodos, um por vez. Um exibe o formulário que acabamos de criar e outro processa seus uploads. O último método nos fornece os uploads de volta, assim você pode ver como isso é feito.

O servlet usa a biblioteca Apache Commons FileUpload. Faça o download da biblioteca e de suas dependências e inclua-as em seu projeto. Quando isso estiver feito, modele o stub na listagem 7:

Listagem 7. DatastoreUploadServlet
import info.johnwheeler.gaestorage.core.*;
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;

@SuppressWarnings("serial")
public class DatastoreUploadServlet extends AbstractUploadServlet {
    private PhotoDao dao = new PhotoDao();
}

Não há nada muito interessante acontecendo aqui. Importamos as classes das quais precisamos e construímos uma PhotoDao para usar mais tarde. DatastoreUploadServlet não fará nenhuma compilação até que implementemos os métodos abstratos. Vamos ver cada um deles, começando com showForm na listagem 8:

Listagem 8. showForm
@Override
protected void showForm(HttpServletRequest req, HttpServletResponse resp) 
    throws ServletException, IOException {
    req.getRequestDispatcher("datastore.jsp").forward(req, resp);        
}

Como se pode ver, showForm simplesmente encaminha para o nosso formulário de upload. handleSubmit, mostrado na listagem 9, está mais envolvido:

Listagem 9. handleSubmit
@Override
protected void handleSubmit(HttpServletRequest req,
    HttpServletResponse resp) throws ServletException, IOException {
    ServletFileUpload upload = new ServletFileUpload();

    try {
        FileItemIterator it = upload.getItemIterator(req);

        Photo photo = new Photo();

        while (it.hasNext()) {
            FileItemStream item = it.next();
            String fieldName = item.getFieldName();
            InputStream fieldValue = item.openStream();

            if ("title".equals(fieldName)) {
                photo.setTitle(Streams.asString(fieldValue));
                continue;
            }

            if ("caption".equals(fieldName)) {
                photo.setCaption(Streams.asString(fieldValue));
                continue;
            }

            if ("file".equals(fieldName)) {
                photo.setContentType(item.getContentType());
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                Streams.copy(fieldValue, out, true);
                photo.setPhotoData(out.toByteArray());
                continue;
            }
        }

        dao.save(photo);
        resp.sendRedirect("datastore?action=display&id=" + photo.getId());            
    } catch (FileUploadException e) {
        throw new ServletException(e);
    }        
}

É uma longa linha de código, mas o que ela faz é simples. O métodohandleSubmit transmite o corpo da solicitação do formulário de upload, extraindo cada valor do formulário em um FileItemStream. Enquanto isso, um Photo é configurado parte por parte. É um pouco chato rolar por cada campo e verificar o que é o quê, mas é assim que se faz com dados de fluxo e com a API de fluxo.

Voltando ao código, quando chegamos ao campo de arquivo, um ByteArrayOutputStream ajuda a repartir os bytes transferidos por download em photoData. Por último, salvamos Photo com PhotoDao e enviamos um redirecionamento, que nos leva à nossa classe abstrata final, showRecord na listagem 10:

Listagem 10. showRecord
@Override
protected void showRecord(long id, HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
    Photo photo = dao.findById(id);
        
    resp.setContentType(photo.getContentType());        
    resp.getOutputStream().write(photo.getPhotoData());
    resp.flushBuffer();                    
}

showRecord olha um Photo e define um cabeçalho com o tipo de conteúdo antes de gravar a array de bytes photoData diretamente na resposta HTTP. flushBuffer empurra qualquer conteúdo restante para o navegador.

A última coisa que precisamos fazer é adicionar código de manipulação de erros para uploads maiores que 1 MB.

Exibição de uma mensagem de erro

Como mencionado anteriormente, o Bigtable impõe um limite de 1 MB, que é um desafio não ultrapassar quando a maioria dos casos de uso envolve imagens. O máximo que podemos fazer é dizer aos usuários para redimensionarem suas imagens e tentarem novamente. Para fins de demonstração, o código na listagem 11 simplesmente exibe uma mensagem de exceção quando uma exceção do GAE é acionada. (Observe que isso é uma manipulação de erros específica para servlet padrão e não específica para GAE.)

Listagem 11. Ocorreu um erro
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;

@SuppressWarnings("serial")
public class ErrorServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) 
        throws ServletException, IOException {
        String message = (String)   
            req.getAttribute("javax.servlet.error.message");
        
        PrintWriter out = res.getWriter();
        out.write("<html>");
        out.write("<body>");
        out.write("<h1>An error has occurred</h1>");                
        out.write("<br />" + message);        
        out.write("</body>");
        out.write("</html>");
    }
}

Não se esqueça de registrar ErrorServlet em web.xml, junto com os outros servlets que criaremos no decorrer deste artigo. O código na listagem 12 registra uma página de erro que aponta de volta para ErrorServlet:

Listagem 12. Registro do erro
<servlet>
    <servlet-name>errorServlet</servlet-name>	  
    <servlet-class>
        info.johnwheeler.gaestorage.servlet.ErrorServlet
    </servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>errorServlet</servlet-name>
    <url-pattern>/error</url-pattern>
</servlet-mapping>

<error-page>
    <error-code>500</error-code>
    <location>/error</location>
</error-page>

Isso encerra nossa rápida introdução ao Bigtable, também conhecido como o armazenamento de dados do GAE. O Bigtable é provavelmente a mais intuitiva das opções de armazenamento do GAE, mas sua desvantagem é o tamanho do arquivo: com 1 MB por arquivo, provavelmente você não irá querer usá-lo para nada maior do que uma miniatura, — se tanto. Em seguida vem o Blobstore, uma outra opção de armazenamento de valor da chave que pode salvar e fornecer arquivos de até 2 GB de tamanho.


Opção nº 2 de armazenamento do GAE: Blobstore

O Blobstore possui a vantagem do tamanho em relação ao Bigtable, mas também seus próprios problemas: a saber, o fato de que ele força o uso de uma URL única de upload, o que torna difícil construir serviços da Web em torno dela. Aí vai um exemplo de como ela se parece:

/_ah/upload/aglub19hcHBfaWRyGwsSFV9fQmxvYlVwbG9hZFNlc3Npb25fXxh9DA

Os clientes de serviços da Web devem pedir a URL antes de usar o POSTnela, o que resulta em uma chamada extra pelo fio. Isso não deve ser um grande problema em muitos aplicativos, mas não é muito elegante. Também poderia ser proibido em casos em que o cliente está executando em GAE, em que as horas de CPU são cobradas. Se você estiver pensando em resolver essas questões construindo um servlet que encaminhe os uploads para a URL única através de URLFetch, pense novamente. O URLFetch possui uma restrição de transferência de 1 MB, de modo que você pode usar o Bigtable também se estiver indo nessa direção. Para quadro de referência, o gráfico na figura 5 mostra a diferença entre uma chamada de serviço da Web de uma e de duas vias:

Figura 5. A diferença entre uma chamada de serviço da Web de uma e de duas vias
A diferença entre uma chamada de serviço da Web de uma e de duas vias

O Blobstore possui seus prós e contras, e você verá mais a respeito nas próximas seções. Mais uma vez, iremos construir um formulário de upload e implementar os três métodos abstratos fornecidos por AbstractUploadServlet— , mas, desta vez, iremos padronizar nosso código para Blobstore.

Um formulário de upload do Blobstore

Não há muito o que adaptar em nosso formulário de upload do Blobstore: basta copiar datastore.jsp para um arquivo chamado blobstore.jsp e depois aumentá-lo com as linhas de código em negrito mostradas na listagem 13:

Listagem 13. blobstore.jsp
<% String uploadUrl = (String) request.getAttribute("uploadUrl"); %><html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>		
        <form method="POST" action="<%= uploadUrl %>"
            enctype="multipart/form-data">
		<!-- labels and fields omitted -->
        </form>
    </body>	
</html>

A URL única de upload é gerada em um servlet, que iremos codificar a seguir. Aqui, essa URL é analisada fora da solicitação e colocada no atributo de ação do formulário. Não temosnenhum controle sobre o servlet do Blobstore para o qual estamos fazendo o upload, então como vamos obter os outros valores do formulário? A resposta é que a API do Blobstore possui um mecanismo de retorno de chamada. Passamos uma URL de retorno de chamada para a API quando a URL única é gerada. Depois do upload, o Blobstore chama o retorno de chamada, passando a solicitação original junto com qualquer blob transferido por upload. Você verá tudo isso em ação quando implementarmos AbstractUploadServlet a seguir.

Upload para o Blobstore

Comece usando a listagem 14 como referência para separar uma classe chamada BlobstoreUploadServlet, que estende AbstractUploadServlet:

Listagem 14. BlobstoreUploadServlet
import info.johnwheeler.gaestorage.core.*;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import com.google.appengine.api.blobstore.*;

@SuppressWarnings("serial")
public class BlobstoreUploadServlet extends AbstractUploadServlet {
    private BlobstoreService blobService = 
        BlobstoreServiceFactory.getBlobstoreService();
    private PhotoDao dao = new PhotoDao();
}

A definição de classe inicial é semelhante ao que fizemos com DatastoreUploadServlet, com a inclusão de uma variável BlobstoreService . É isso que gera URL única em showForm na listagem 15:

Listagem 15. showForm para blobstore
@Override
protected void showForm(HttpServletRequest req, HttpServletResponse resp) 
    throws ServletException, IOException {
    String uploadUrl = blobService.createUploadUrl("/blobstore");
    req.setAttribute("uploadUrl", uploadUrl);
    req.getRequestDispatcher("blobstore.jsp").forward(req, resp);
}

O código na listagem 15 cria uma URL de upload e a configura na solicitação. O código encaminha para o formulário criado na listagem 13, no qual a URL de upload é esperada. A URL de retorno de chamada é configurada para esse contexto do servlet, conforme definido em web.xml. Assim, quando osPOSTs voltam, acabamos em handleSubmit, mostrada na listagem 16:

Listagem 16. handleSubmit para Blobstore
@Override
protected void handleSubmit(HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
    Map<String, BlobKey> blobs = blobService.getUploadedBlobs(req);
    BlobKey blobKey = blobs.get(blobs.keySet().iterator().next());

    String photoPath = blobKey.getKeyString();
    String title = req.getParameter("title");
    String caption = req.getParameter("caption");
    
    Photo photo = new Photo(title, caption);
    photo.setPhotoPath(photoPath);
    dao.save(photo);

    resp.sendRedirect("blobstore?action=display&id=" + photo.getId());
}

getUploadedBlobs retorna um Map do BlobKeys. Como o nosso formulário de upload suporta um único upload, pegamos o único BlobKey esperado e colocamos uma representação de sequência dele na variável photoPath . Em seguida, o restante dos campos é analisado em variáveis e configurado em uma nova instância Photo . A instância é salva no armazenamento de dados antes de redirecionar para showRecord na listagem 17:

Listagem 17. showRecord para blobstore
@Override
protected void showRecord(long id, HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
    Photo photo = dao.findById(id);
    String photoPath = photo.getPhotoPath();

    blobService.serve(new BlobKey(photoPath), resp);
}

Em showRecord, o Photo que acabamos de salvar em handleSubmit é recarregado a partir do Blobstore. Os bytes reais de qualquer coisa que tenha sido transferida por upload não são armazenados no bean, como eram no Bigtable. Em vez disso, um BlobKey é reconstruído com photoPath e usado para fornecer uma imagem para o navegador.

O Blobstore torna o trabalho com uploads baseados em formulário muito fácil, mas os uploads baseados em serviço da Web são uma outra coisa. A seguir veremos o Google Storage for Developers, que nos traz a questão exatamente oposta: os uploads baseados em formulários requerem um pouco de hacking, enquanto os uploads baseados em serviço são fáceis.


Opção nº 3 de armazenamento do GAE: Google Storage

O Google Storage for Developers é a mais poderosa das três opções de armazenamento do GAE, e é fácil de usar depois de ter tirado algumas dúvidas do caminho. O Google Storage tem muito em comum com o Amazon S3; na verdade, os dois usam o mesmo protocolo e possuem a mesma interface RESTful, de modo que as bibliotecas feitas para se trabalhar com o S3, como a JetS3t, também funcionam no Google Storage. Infelizmente, no momento em que estou escrevendo este artigo, essas bibliotecas não funcionam de modo confiável no Google App Engine porque elas realizam operações não permitidas, como encadeamentos de spawn. Assim, no momento, temos de trabalhar com a interface RESTful e fazer um pouco do trabalho pesado que essas APIs poderiam fazer.

Mas vale a pena o trabalho do Google Storage, principalmente porque ele suporta controles de acesso poderosos através de listas de controle de acesso (ACLs). Com as ACLs, é possível conceder acesso somente leitura e leitura/gravação para objetos, assim você pode tornar as fotos públicas ou privadas facilmente, como no Facebook e no Flickr. As ACLs estão fora do escopo deste artigo, assim, todo upload será considerado de acesso público, somente leitura. Consulte a documentação on-line do Google Storage (em Recursos ) para saber mais sobre as ACLs.

Sobre o Google Storage

Lançado como edição prévia em maio de 2010, o Google Storage for Developers está disponível atualmente somente para um número limitado de desenvolvedores nos Estados Unidos, e há uma lista de espera para a prévia. Como o Google Storage está ainda na infância, ele também apresenta alguns desafios de implementação, que discutirei nesta seção. A falta de um caminho de integração bem-definido entre o Google Storage e o GAE significa codificação extra, mas para alguns casos de uso — como aqueles que requerem controle de acesso — ele vale a pena. Espero ver bibliotecas de integração em um futuro próximo.

Diferentemente do Blobstore, o Google Storage é compatível por padrão para uso por serviço da Web e clientes de navegador. Os dados são enviados através de PUT ouPOST. de RESTful. A primeira opção é para clientes de serviço da Web que podem controlar como as solicitações são estruturadas e como os cabeçalhos são gravados. A segunda opção, que exploraremos aqui, é para uploads baseados em navegador. Precisaremos de um hack JavaScript para processar o formulário de upload, que apresenta algumas complicações, como iremos ver.

Hack do formulário de upload do Google Storage

Diferentemente do Blobstore, o Google Storage não encaminha uma URL de retorno de chamada depois de o POSTter sido usado nela. Em vez disso, ele emite um redirecionamento para a URL que especificarmos. Isso apresenta um problema, já que os valores dos formulários não são transportados no redirecionamento. A forma de lidar com isso é criar dois formulários na mesma página da Web — um contendo os campos de texto de título e legenda, o outro com o campo de upload de arquivo e os parâmetros exigidos pelo Google Storage. Usaremos então o Ajax para enviar o primeiro formulário. Quando o retorno de chamada do Ajax for chamado, enviaremos o segundo formulário de upload.

Como esse formulário é mais complicado do que os dois últimos, iremos construí-lo passo a passo. Primeiro, extraímos alguns valores que são definidos por um servlet de encaminhamento que ainda não foi construído, mostrado na listagem 18:

Listagem 18. Extração de valores do formulário
<% 
String uploadUrl = (String) request.getAttribute("uploadUrl");
String key = (String) request.getAttribute("key");
String successActionRedirect = (String) 
    request.getAttribute("successActionRedirect");
String accessId = (String) request.getAttribute("GoogleAccessId");
String policy = (String) request.getAttribute("policy");
String signature = (String) request.getAttribute("signature");
String acl = (String) request.getAttribute("acl");
%>

A uploadUrl contém o terminal REST do Google Storage. A API fornece os dois mostrados abaixo. Qualquer um é aceitável, mas somos responsáveis por substituir os componentes em itálico por nossos próprios valores:

  • bucket.commondatastorage.googleapis.com/object
  • commondatastorage.googleapis.com/bucket/object

As variáveis restantes são parâmetros exigidos do Google Storage:

  • key: O nome dos dados transferidos por upload no Google Storage.
  • success_action_redirect: Para onde redirecionar quando o upload estiver concluído.
  • GoogleAccessId: Uma chave de API atribuída ao Google.
  • policy: Uma cadeia de caractere de base JSON com 64 caracteres codificados, limitando como os dados são transferidos por upload.
  • signature: A política assinalada com um algoritmo hash e uma base de 64 caracteres codificados. Usada para autenticação.
  • acl: Uma especificação da lista de controle de acesso.

Dois formulários e um botão de enviar

O primeiro formulário na listagem 19 contém somente os campos de título e legenda. As tags do <html> circundante e do <body> foram omitidas.

Listagem 19. O primeiro formulário de upload
<form id="fieldsForm" method="POST">
    <table>
        <tr>	
            <td>Title</td>
            <td><input type="text" name="title" /></td>
        </tr>
        <tr>	
            <td>Caption</td>
            <td>
                <input type="hidden" name="key" value="<%= key %>" />	
                <input type="text" name="caption" />
            </td>
        </tr>			
    </table>		
</form>

Não há muito o que dizer sobre esse formulário, exceto que ele usa o POSTpara postar para si mesmo. Vamos para o formulário da listagem 20, que é maior porque contém uma meia dúzia de campos de entrada ocultos:

Listagem 20. O segundo formulário com campos ocultos
<form id="uploadForm" method="POST" action="<%= uploadUrl %>" 
    enctype="multipart/form-data">
    <table>
        <tr>
            <td>Upload</td>
            <td>
                <input type="hidden" name="key" value="<%= key %>" />
                <input type="hidden" name="GoogleAccessId" 
                    value="<%= accessId %>" />
                <input type="hidden" name="policy" 
                    value="<%= policy %>" />
                <input type="hidden" name="acl" value="<%= acl %>" />
                <input type="hidden" id="success_action_redirect" 
                    name="success_action_redirect" 
                    value="<%= successActionRedirect %>" />
                <input type="hidden" name="signature"
                    value="<%= signature %>" />
                <input type="file" name="file" />
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="button" value="Submit" id="button"/>
            </td>
        </tr>
    </table>
</form>

Os valores extraídos no scriptlet JSP (na listagem 18) são colocados nos campos ocultos. A entrada do arquivo está na parte inferior. O botão de enviar é um botão antigo simples, que não fará nada até que o ajustemos com JavaScript, como mostrado na listagem 21:

Listagem 21. Envio do formulário de upload
<script type="text/javascript" 
src="https://Ajax.googleapis.com/Ajax/libs/jquery/1.4.3/jquery.min.js">
</script>
<script type="text/javascript">
    $(document).ready(function() {			
        $('#button').click(function() {
            var formData = $('#fieldsForm').serialize();
            var callback = function(photoId) {
                var redir = $('#success_action_redirect').val() +
                    photoId;
                $('#success_action_redirect').val(redir)
                $('#uploadForm').submit();
             };
			
             $.post("gstorage", formData, callback);
         });
     });
</script>

O JavaScript na listagem 21 é escrito com JQuery. Mesmo que não tenha usado a biblioteca, o código não deve ser difícil de entender. A primeira coisa que o código faz é importar a JQuery. Depois, um listener de clique é instalado no botão, de modo que quando o botão é clicado, o primeiro formulário é enviado via Ajax. A partir daí, chegamos ao método handleSubmit do servlet (que construiremos em breve), no qual um Photo é construído e salvo no armazenamento de dados. Finalmente, a nova Photo ID é retornada para o retorno de chamada e anexada à URL emsuccess_action_redirect antes de o formulário de upload ser enviado. Dessa forma, quando voltamos do redirecionamento, podemos olhar Photo e exibir sua imagem. A figura 6 mostra toda a sequência de eventos:

Figura 6. Um diagrama de sequência mostrando o caminho de chamada do JavaScript
Um diagrama de sequência mostrando o caminho de chamada do JavaScript

Com o formulário sendo cuidado, precisamos de uma classe do utilitário para criar e assinar documentos sobre políticas. Depois podemos ir para a subclasse AbstractUploadServlet.

Criação e assinatura de um documento sobre políticas

Os documentos sobre políticas limitam os uploads. Por exemplo, devemos especificar o tamanho dos uploads ou que tipos de arquivos são aceitáveis, e podemos até impor restrições em nomes de arquivos. Depósitos públicos não exigem documentos sobre políticas, mas os depósitos privados, como o Google Storage, exigem. Para colocar as coisas em movimento, elimine uma classe do utilitário chamada GSUtils baseada no código da listagem 22:

Listagem 22. GSUtils
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import com.google.appengine.repackaged.com.google.common.util.Base64;

private class GSUtils {
}

Dado que as classes de utilitário são normalmente compostas somente de métodos estáticos, é uma boa ideia privatizar seus construtores padrão para evitar instanciação. Com a classe separada, podemos voltar nossa atenção para a criação de documento sobre políticas.

O documento sobre políticas é formatado em JSON, mas o JSON é bastante simples, de modo que não precisamos recorrer a nenhuma biblioteca especial. Em vez disso, podemos fazer as coisas à mão, com um simples StringBuilder. Primeiro, temos que construir uma data de ISO8601 e definir o documento sobre políticas que será expirado por ela. Os uploads não terão êxito quando o documento sobre políticas expirar. Depois, temos que colocar as restrições sobre as quais falamos anteriormente, que são chamadas de condições no documento sobre políticas. Finalmente, o documento é codificado em uma base de 64 e retornado para o responsável pela chamada.

Inclua o método da listagem 23 em GSUtils:

Listagem 23. Criação de um documento sobre políticas
public static String createPolicyDocument(String acl) {
    GregorianCalendar gc = new GregorianCalendar();
    gc.add(Calendar.MINUTE, 20);

    DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    df.setTimeZone(TimeZone.getTimeZone("GMT"));
    String expiration = df.format(gc.getTime());

    StringBuilder buf = new StringBuilder();
    buf.append("{\"expiration\": \"");
    buf.append(expiration);
    buf.append("\"");
    buf.append(",\"conditions\": [");
    buf.append(",{\"acl\": \"");
    buf.append(acl);
    buf.append("\"}");        
    buf.append("[\"starts-with\", \"$key\", \"\"]");
    buf.append(",[\"starts-with\", \"$success_action_redirect\", \"\"]");
    buf.append("]}");

    return Base64.encode(buf.toString().replaceAll("\n", "").getBytes());
}

Usamos um GregorianCalendar que foi ajustado para 20 minutos mais tarde para construir a data de expiração. O código é kludgy, que pode ser impresso no console, copiado e executado por meio de uma ferramenta como o JSONLint (consulte Recursos ). Em seguida, passamos a acl no documento sobre políticas para evitar o hardcoding nele. Tudo o que for variável deve ser passado como argumento do método, como acl. Finalmente, o documento é codificado em uma base de 64 antes de ser retornado para o responsável pela chamada.Consulte a documentação do Google Storage para obter mais informações sobre o que é permitido no documento sobre políticas.

Secure Data Connector do Google

Não iremos trabalhar com o Secure Data Connector do Google neste artigo, mas vale a pena dar uma olhada nele se estiver planejando usar o Google Storage. O SDC torna mais fácil acessar dados em seus próprios sistemas, mesmo que estes estejam por trás de um firewall.

Autenticação no Google Storage

Os documentos sobre políticas oferecem duas funções. Além de impingirem políticas, eles são a base das assinaturas que geramos para autenticar os uploads. Quando nos inscrevemos no Google Storage, recebemos uma chave secreta que somente nós e o Google sabemos. Assinamos o documento do nosso lado com a chave secreta e o Google o assina com a mesma chave. Se as assinaturas corresponderem, o upload é permitido. A figura 7 oferece uma boa visão desse ciclo:

Figura 7. Como os uploads são autenticados no Google Storage
Como os uploads são autenticados no Google Storage

Para gerar a assinatura, usamos os pacotes javax.crypto e java.security que importamos enquanto separávamos GSUtils. A listagem 24 mostra os métodos:

Listagem 24. Assinatura de um documento sobre políticas
public static String signPolicyDocument(String policyDocument,
    String secret) {
    try {
        Mac mac = Mac.getInstance("HmacSHA1");
        byte[] secretBytes = secret.getBytes("UTF8");
        SecretKeySpec signingKey = 
            new SecretKeySpec(secretBytes, "HmacSHA1");
        mac.init(signingKey);
        byte[] signedSecretBytes = 
            mac.doFinal(policyDocument.getBytes("UTF8"));
        String signature = Base64.encode(signedSecretBytes);
        return signature;
    } catch (InvalidKeyException e) {
        throw new RuntimeException(e);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
    }
}

Um hashing seguro em código Java envolve rigmarole que eu prefiro não discutir neste artigo. O que importa é que a listagem 24 mostra como isso é feito adequadamente, e que o hash deve ser codificado em uma base de 64 antes de ser retornado.

Com esses pré-requisitos atendidos, voltamos a um território familiar: implementar os três métodos abstratos para fazer upload e recuperar arquivos do Google Storage.

Upload para o Google Storage

Comece separando uma classe chamada GStorageUploadServlet baseada no código da listagem 25:

Listagem 25. GStorageUploadServlet
import info.johnwheeler.gaestorage.core.GSUtils;
import info.johnwheeler.gaestorage.core.Photo;
import info.johnwheeler.gaestorage.core.PhotoDao;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.UUID;

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

@SuppressWarnings("serial")
public class GStorageUploadServlet extends AbstractUploadServlet {
    private PhotoDao dao = new PhotoDao();
}

O métodoshowForm , mostrado na listagem 26, configura os parâmetros que precisamos passar para o Google Storage através do formulário de upload:

Listagem 26. showForm para Google Storage
@Override
protected void showForm(HttpServletRequest req, HttpServletResponse resp) 
    throws ServletException, IOException {
    String acl = "public-read";
    String secret = getServletConfig().getInitParameter("secret");
    String accessKey = getServletConfig().getInitParameter("accessKey");
    String endpoint = getServletConfig().getInitParameter("endpoint");
    String successActionRedirect = getBaseUrl(req) + 
        "gstorage?action=display&id=";
    String key = UUID.randomUUID().toString();
    String policy = GSUtils.createPolicyDocument(acl);
    String signature = GSUtils.signPolicyDocument(policy, secret);

    req.setAttribute("uploadUrl", endpoint);
    req.setAttribute("acl", acl);
    req.setAttribute("GoogleAccessId", accessKey);
    req.setAttribute("key", key);
    req.setAttribute("policy", policy);
    req.setAttribute("signature", signature);
    req.setAttribute("successActionRedirect", successActionRedirect);

    req.getRequestDispatcher("gstorage.jsp").forward(req, resp);
}

Observe que o acl é configurado para leitura pública, assim, qualquer coisa que for transferida por upload será visualizada por qualquer um. As próximas três variáveis, secret, accessKey e endpoint, são usadas para obter a permissão e autenticar com o Google Storage. Eles são extraídos de init-params declarados em web.xml; consulte o código de amostra para obter detalhes.Lembre-se de que, diferentemente do Blobstore, que encaminha para uma URL que nos coloca em showRecord, o Google Storage emite um redirecionamento. A URL de redirecionamento é armazenada em successActionRedirect. successActionRedirect recorre ao método auxiliar na listagem 27 para construir a URL de redirecionamento.

Listagem 27. getBaseUrl()
private static String getBaseUrl(HttpServletRequest req) {
    String base = req.getScheme() + "://" + req.getServerName() + ":" + 
        req.getServerPort() + "/";
    return base;
}

O método auxiliar pesquisa a solicitação recebida para construir a URL de base antes de devolver o controle para showForm. Ao retornar, uma chave é criada com um identificador exclusivo universal ou UUID, que é uma String exclusiva. Em seguida, a política e a assinatura são geradas com a classe do utilitário que construímos. Finalmente, configuramos os atributos de pedido do JSP antes de encaminhar para ele.

A listagem 28 mostra handleSubmit:

Listagem 28. handleSubmit para Google Storage
@Override
protected void handleSubmit(HttpServletRequest req, HttpServletResponse 
    resp) throws ServletException, IOException {
    String endpoint = getServletConfig().getInitParameter("endpoint");
    String title = req.getParameter("title");
    String caption = req.getParameter("caption");
    String key = req.getParameter("key");

    Photo photo = new Photo(title, caption);
    photo.setPhotoPath(endpoint + key);
    dao.save(photo);

    PrintWriter out = resp.getWriter();
    out.println(Long.toString(photo.getId()));
    out.close();
}

Lembre-se de que quando o primeiro formulário é enviado, somos colocados em handleSubmit por um POST. do Ajax. O upload em si não é identificado ali, mas separadamente, em um retorno de chamada do Ajax. handleSubmit apenas analisa o primeiro formulário, constrói um Photo e o salva no armazenamento de dados. Depois, a ID de Photoé retornada para o retorno de chamada do Ajax, escrevendo-a no corpo de resposta.

No retorno de chamada, o formulário de upload é enviado para o terminal do Google Storage. Quando o Google Storage processa o upload, ele é configurado para emitir um redirecionamento de volta para showRecord, na listagem 29:

Listagem 29. showRecord para Google Storage
@Override
protected void showRecord(long id, HttpServletRequest req, 
    HttpServletResponse resp) throws ServletException, IOException {
    Photo photo = dao.findById(id);
    String photoPath = photo.getPhotoPath();
    resp.sendRedirect(photoPath);
}

showRecord olha Photo e redireciona para seu photoPath. photoPath aponta para nossa imagem hospedada nos servidores do Google.


Conclusão

Examinamos três opções de armazenamento centradas no Google e avaliamos seus prós e contras. O Bigtable é fácil de se trabalhar, mas impõe um limite de tamanho de arquivo de 1 MB. Os blobs no Blobstore podem ter até 2 GB por peça, mas a URL única é difícil de se trabalhar nos serviços da Web. Finalmente, o Google Storage for Developers é a opção mais robusta. Pagamos somente pelo armazenamento que usamos e o céu é o limite quando se trata da quantidade de dados que pode ser armazenada em um único arquivo. O Google Storage também é a solução mais complexa para se trabalhar, porque suas bibliotecas atualmente não suportam o GAE. O suporte de uploads baseados em navegador também não é a coisa mais fácil do mundo.

À medida que o Google App Engine se torna uma plataforma de desenvolvimento mais popular para desenvolvedores de Java, entender suas várias opções de armazenamento é essencial. Neste artigo, vimos exemplos de implementação simples para Bigtable, Blobstore e Google Storage for Developers. Quer você escolha uma opção de armazenamento e fique com ela, quer você use cada uma para um caso de uso diferente, agora você possui as ferramentas necessárias para armazenar montanhas de dados no GAE.


Download

DescriçãoNomeTamanho
Sample code for this articlej-gaestorage.zip12KB

Recursos

Aprender

Obter produtos e tecnologias

Discutir

  • Participe da comunidade My developerWorks. Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que acessar o developerWorks, um perfil será criado para você. Informações do seu perfil (tais como: nome, país / região, e empresa) estarão disponíveis ao público, que poderá acompanhar qualquer conteúdo que você publicar. Seu perfil no developerWorks pode ser atualizado a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java, Cloud computing
ArticleID=656736
ArticleTitle=Armazenamento do GAE com Bigtable, Blobstore e Google Storage
publish-date=07222011