Em um dos meus clientes é muito comum o desenvolvimento de aplicações Java voltadas para o ambiente desktop, aonde não raro enfrento problemas sérios de performance e consumo de recursos. O objetivo deste post é expor como usando o recurso de serialização de objetos oferecido pela plataforma Java consegui resolver (ou ao menos minimizar ao extremo) este desafio.
Imagine a seguinte situação: sua aplicação desktop acessa informações presentes em um banco de dados relacional. Dado que você precisa de produtividade, opta por usar uma ferramenta ORM como Hibernate ou JPA. Não resta dúvidas de que trata-se de um recurso maravilhoso, mas infelizmente traz consigo um custo de memória (precisamos carregar o motor de mapeamento objeto relacional, além disto, muitas vezes usamos cacheamento que, ao menos em parte, fica em memória) e performance (trata-se de mais uma camada no sistema). Em algumas situações causa um aumento no tempo de carregamento dos nossos sistemas tão grande que, não raro, você se vê questionando o ganho real do seu uso.
Imagine agora outra situação: sua aplicação trabalha apenas com JDBC e, com isto, sua performance é superior à obtida usando JPA ou Hibernate (apesar de sua existência neste caso ser mais infeliz), porém há limitações no número de conexões que podem ser feitas ao SGBD e, pior ainda, o tempo gasto estabelecendo a conexão com o SGBD é alto. Em situações como esta, o tal “ganho de performance” por não se estar usando Hibernate se perde totalmente, pois você nem tem o cacheamento automático provido pelo ORM.
Em ambos os casos, como resolver o problema? Simples: evitando ao máximo que seja necessário estabelecer QUALQUER tipo de conexão com o servidor.
Entra em cena a serialização de objetos
Todo sistema possui informações cuja alteração seja rara, ou seja, registros que apenas são inseridos no banco de dados e em raras situações tenham seus dados alterados. Como exemplos posso citar um cadastro de fornecedores ou endereços. Tratam-se de informações que são usadas, em 90% dos casos, apenas para consulta ou na composição de tipos mais complexos. Pergunte-se: é realmente necessário sempre pedir ao SGBD estes dados?
Nestes casos, a serialização de objetos cai como uma luva. E se você está em um ambiente corporativo, é possível inclusive fazer com que um único usuário beneficie todos os demais com um custo adicional mínimo. Vou ilustrar a situação usando como exemplo uma aplicação desktop que precise expor em um formulário uma caixa de seleção contendo a listagem de fornecedores presentes no banco de dados.
Vamos supor que esta nossa aplicação use o Hibernate. Neste caso, ao criar uma nova instância do formulário, precisamos obter a listagem de fornecedores seguindo o seguinte procedimento:
- É criada uma consulta no Hibernate usando criteria ou HQL
- Esta consulta é transformada em comandos SQL que serão enviados ao driver JDBC
- O driver JDBC envia a consulta para o SGBD
- O SGBD executa a consulta e retorna o resultado para o cliente
- O driver JDBC traduz o resultado enviado pelo SGBD para um objeto java.sql.Recordset
- O Hibernate cria novas instâncias da classe Fornecedor e, via reflexão, preenche todos os seus atributos
- E, finalmente, é retornada uma lista contendo o resultado para o cliente
Se esta for a primeira consulta feita pela aplicação, o seu usuário ainda vai ter de sofrer um pouquinho com o carregamento do SessionFactory do Hibernate.
Agora, quer ver como as coisas ficam mais simples usando serialização? Suponha que eu já tenha serializado esta lista de fornecedores em um arquivo presente em minha rede. O seguinte procedimento seria adotado.
- É verificado se o arquivo existe na rede.
- Se o arquivo existe, então este é desserializado em uma lista de fornecedores e retornado ao cliente.
- Se o arquivo não existe, execute o longo procedimento exposto acima novamente, com a diferença de que, agora, você serializará o resultado em arquivo no final.
Observe que interessante: não precisei carregar o Hibernate nem comunicar-me com qualquer servidor. Claro, se o arquivo não existisse, todo o processo citado acima seria necessário, porém em um ambiente de rede, só precisaria ser feito uma vez pelo primeiro usuário que necessitasse da lista de fornecedores. Dai pra frente, todos os demais usuários obteriam o ganho de performance que apresentei.
Mas o que é serialização de objetos
Ok, talvez você esteja apenas começando a aprender Java e não saiba o que é esta tal de serialização. Porcamente falando, serialização é o processo de salvar em arquivo um objeto presente na memória do seu computador.
Se eu quisesse salvar a minha listagem de fornecedores em um arquivo, o código abaixo daria conta do recado:
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(new FileOutputStream(new File("fornecedores"))); oos.writeObject(listaFornecedores); oos.close();
E, para transformar o meu arquivo em objeto, de novo apenas três linhas fariam o trabalho pra mim.
java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new FileInputStream(new File("fornecedores"))); List<Fornecedor> lista = (List<Fornecedor>) ois.readObject(); ois.close();
Detalhe: só podem ser serializados objetos que implementem a interface java.io.Serializable.
Omiti as excessões apenas para expor como é um recurso absurdamente fácil de ser usado.
Armadilhas a serem evitadas
O principal desafio desta abordagem é manter o cache sempre atualizado. Tenho duas soluções para este problema. A mais simples é de tempos em tempos apagar os arquivos de cache da rede. A mais complexa consiste em, toda vez que for persistir uma novo registro no banco de dados, apagar os arquivos de cache.
Dica: evite ao máximo chamar a função exist() da classe java.io.File, pois esta é absurdamente cara, visto que a cada chamada acessa o gerenciador de segurança do Java. Dado que o objetivo de qualquer sistema é expor os dados para o usuário, você pode sempre, ao verificar a existência do cache, escrever código similar ao exposto abaixo:
private List<Fornecedor> getListaCache() { try { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("fornecedores"))); List<Fornecedor> resultado = (List<Fornecedor>) ois.readObject(); ois.close(); return resultado; } catch (Throwable t) { return null; } } public List<Fornecedor> getFornecedores() { List<Fornecedor> no_cache = getListaCache(); if (no_cache != null) return no_cache; // caso contrário, procedimento padrão, serializando o resultado no final }
Outra armadilha: em um ambiente de rede, evite serializar os dados no HD local do usuário. Sempre que possível, opte por usar um drive de rede. Assim, caso seja necessário uma atualização do cache imediata, você, como administrador do sistema, em último caso sempre poderá apagar o arquivo de cache, forçando o cliente a recriá-lo novamente.
Fique atento também ao que será serializado. Dados que apenas ocasionalmente são consultados não são um bom alvo. Opte apenas por aquelas informações que são vitais ao uso do seu sistema e cuja atualização seja rara.
E, a principal armadilha: JAMAIS use esta técnica em entidades que precisem ser constantemente atualizadas na base de dados.
Padrão: buscando um único registro
Há situações em que não é necessário buscar uma lista de registros, mas apenas um, o que normalmente é feito executando uma consulta por chave primária. Nestes casos, adotei um padrão bastante simples. Crio um diretório na rede chamado cache e, dentro dele, um subdiretório que identifique o tipo da minha entidade. No caso de fornecedores, chamo este diretório, por exemplo, de /cache/fornecedor. No interior deste diretório armazeno arquivos cujo nome corresponda à chave primária da entidade que desejo buscar no banco de dados.
Mas se o Hibernate já possui sistema de cache, pra que usar serialização?
Como diriam os candidatos a presidente em um debate… esta é uma pergunta importantíssima.
Simples: porque a serialização, tal como estou expondo neste post, deve ser usada ANTES da primeira chamada ao Hibernate, pois assim, em algumas situações, consegue-se evitar o carregamento deste framework e, consequentemente, nosso usuário final terá um sistema muito mais leve e responsível.
Se sua aplicação vai ser executada em um servidor nem sei se o uso desta técnica trará algum resultado que realmente valha à pena.
Conclusões
Uso esta técnica para resolver os problemas de performance que enfrento em aplicações desktop, aonde normalmente os recursos computacionais podem ser mais limitados (as malditas “máquinas brancas”). O que pude observar é que, nestes casos, obtenho ganhos de performance astronômicos, pois a comunicação com o SGBD é reduzida a apenas o estritamente necessário.
Outro ganho que obtenho é o tempo de inicialização da aplicação, pois é muito comum neste momento ser necessária a execução de alguma consulta no SGBD. Sendo assim, esta é uma técnica que, ao menos por enquanto, cai como uma luva quando aplicada ao ambiente desktop.
Aos que queiram se aprofundar mais no assunto, sugiro a leitura deste post: “The Java serialization algorithm revealed”, que explica com detalhes o processo de serialização: http://www.javaworld.com/community/node/2915
Agora: sabem o que me choca nesta história toda? Este recurso maravilhoso oferecido pela plataforma é raríssimas vezes usado pelos desenvolvedores. Da pra acreditar???
Deixe uma resposta