Atenção: esse artigo tem um vídeo complementar. Clique e assista!
Este artigo aborda a Compiler API, que possibilita compilar classes de uma aplicação durante a sua execução. Um framework que gera classes DAO (Data Access Object) é criado para exemplificar o uso da API, bem como uma aplicação que utiliza este framework.
Em que situação o tema é útil:
Este tema é útil para desenvolvedores que têm interesse em aprender a utilizar a Compiler API, a fim de gerar classes durante a execução das aplicações.
Resumo DevMan:
Na grande maioria das vezes, todas as classes de uma aplicação são criadas e compiladas antes que ela seja executada. No entanto, existem alguns casos onde determinadas classes precisam ser geradas em tempo de execução. Formas de fazer isto já existem há algum tempo, através da utilização de bibliotecas de terceiros. Mas a partir da versão 6 do Java, a Compiler API foi incorporada à linguagem, permitindo suporte a este recurso de forma nativa.
Este artigo aborda esta API em detalhes, e também propõe um framework de geração de classes DAO (Data Access Object) em tempo de execução, a fim de mostrar, de forma prática, a utilização da Compiler API.
Praticamente toda aplicação Java desenvolvida segue o mesmo processo até ser executada. Tudo começa com a escrita dos códigos-fonte das classes, que posteriormente são compilados para uma linguagem intermediária chamada bytecode. Depois, no momento em que a aplicação começa a ser executada, a JVM faz o carregamento e interpretação destas classes compiladas, a fim de que tudo funcione de acordo com o que foi implementado.
Embora este seja o processo mais comum, ele não é o único. A partir do Java 6 foi introduzida na linguagem a Compiler API, que possibilita uma variação no processo descrito anteriormente. Com esta API, classes da aplicação podem ser definidas, compiladas e terem seus bytecodes carregados pela JVM enquanto a aplicação é executada. Na prática, isto significa que estas classes não existem durante a compilação.
A apesar de a Compiler API ser relativamente recente (a partir da versão 6 do Java), a ideia de gerar classes em tempo de execução não é nova. Existem diversos frameworks e APIs que tiram proveito deste recurso. Para que isso fosse possível há alguns anos, eles utilizavam bibliotecas desenvolvidas por terceiros como o CGLIB e o ASM, as quais permitem a manipulação direta de bytecode; ou o Apache Commons JCI, que é uma interface para um compilador Java já existente, responsável por gerar as classes. Para mais informações sobre estas APIs, consulte a seção de Links.
Um exemplo bastante claro de utilização deste recurso vem dos JSPs, que são utilizados para renderizar páginas web dinâmicas para os usuários. Quando um JSP é requisitado pela primeira vez, o servidor executa um processo de conversão do arquivo JSP para uma classe de Servlet, compila este arquivo e o carrega. Isto possibilita que o desenvolvedor faça a programação da página como se ela fosse um JSP, mas na hora da execução a página dinâmica é gerada por um Servlet. A vantagem disso é que a facilidade de implementar a estrutura de uma página web dinâmica em JSP é muito maior.
Outro framework que utiliza a geração dinâmica de classes é o Hibernate. Para que ele possa fazer o gerenciamento de persistência das entidades, o Hibernate precisa criar classes de proxy, as quais agem como classes intermediárias no momento em que o framework precisa acessar as entidades reais, criadas pelo desenvolvedor. Os proxies são gerados em tempo de execução e escondem do programador a complexidade deste processo, uma vez que estas classes não existem antes do código ser executado.
Por fim, outro exemplo de API que utiliza este recurso é o RMI (Remote Method Invocation). Ele possibilita que o usuário faça chamadas de métodos em objetos que podem estar localizados em outro computador da rede. Mas para quem faz a chamada, toda esta complexidade da invocação remota é transparente, uma vez que o código é escrito como se ela fosse local (dentro da própria JVM). Isto só é possível devido a um componente chamado stub, que é uma espécie de objeto local que representa o objeto remoto. É dele a responsabilidade de fazer com que a requisição ao objeto real seja feita via rede e também de receber o retorno da invocação. Para que o desenvolvedor não se preocupe com todos estes detalhes, os stubs necessários são gerados durante a execução da aplicação.
Com base nos exemplos citados, é possível perceber que a compilação dinâmica tem os seus benefícios. A geração de código em tempo de execução pode ser muito útil no desenvolvimento de frameworks que buscam reduzir a complexidade de escrita de código por parte dos desenvolvedores. Pode também ser utilizada para gerar código Java a partir de uma linguagem qualquer, que poderia ser específica de um domínio ou aplicação. E ferramentas que automatizam a geração de código também podem se beneficiar deste recurso, podendo gerar classes a partir de templates pré-definidos.
Este artigo é dividido em três partes. Na primeira, o processo de compilação dinâmica com a Compiler API será explicado em detalhes. Na segunda, um framework que gera classes DAO (Data Access Object) será proposto, com o objetivo de mostrar um exemplo prático da compilação em tempo de execução. E na terceira e última parte, a utilização deste framework será demonstrada através da criação da camada DAO de uma aplicação fictícia, cuja função é gerenciar infrações de trânsito. A proposta desta divisão é mostrar não só a teoria, mas também os benefícios práticos da Compiler API.
A Java Compiler API
Como já foi mencionado, existem diversas formas de gerar classes em tempo de execução no Java. Antes da versão 6, nenhuma delas era padrão da linguagem e sempre houve a dependência de bibliotecas criadas por terceiros.
Com o intuito de padronizar este recurso e torná-lo nativo do Java, foi criada a JSR 199, que define a Compiler API. Implementada a partir do Java 6 e localizada no pacote javax.tools, ela tornou possível compilar classes durante a execução da aplicação e permitir que elas sejam carregadas pela JVM para serem utilizadas. Com esta API, os desenvolvedores não precisam mais recorrer a bibliotecas externas, que podem não acompanhar os lançamentos de novas versões da linguagem. Além disso, a Compiler API é capaz de gerar classes a partir de códigos-fonte armazenados em qualquer lugar (como arquivos ou até strings), e em nenhum momento o desenvolvedor precisa se envolver diretamente com o bytecode gerado. Isto reduz significativamente a complexidade de implementação.
A utilização desta API exige a criação de algumas classes, o que resulta em um volume considerável de código. Por este motivo, o ideal é encapsular toda a implementação necessária, de forma que ela possa ser reaproveitada em diversas aplicações da forma mais simples possível. Ao final desta primeira parte do artigo, você terá esta biblioteca criada e pronta para ser utilizada em diversos projetos.
Independente da complexidade envolvida no processo de compilação dinâmica, a essência deste recurso é simples: a partir de um código-fonte é gerada uma classe compilada. Com base nisso, a primeira classe importante que será criada é denominada JavaCodeCompiler. Ela possui apenas o método compile(), que recebe os nomes do pacote e da classe que será compilada e o código-fonte. O seu retorno é um objeto Class, que representa a classe já compilada. Outro detalhe é que JavaCodeCompiler pode ser parametrizada, pois suporta o uso de generics. Isto permite definir a classe de retorno do método.
Definir a classe retornada pelo método compile() pode parecer estranho à primeira vista, já que não é possível referenciar no código-fonte uma classe que só vai existir quando a aplicação for iniciada. No entanto, é bastante comum que as classes compiladas dinamicamente sejam subclasses ou implementações de interfaces. Neste caso, o tipo parametrizado especificado é o mais genérico, que já é conhecido no momento da compilação.
Veja um exemplo de utilização da classe JavaCodeCompiler na Listagem 1. Ela é instanciada utilizando Runnable como tipo parametrizado, o que significa que o método compile() vai retornar uma classe que implementa esta interface. Olhando atentamente para o código, é possível perceber que uma classe chamada RunnableImpl será criada, e que ela realmente implementa a interface Runnable.
Listagem 1. Exemplo de utilização da classe Compiler.
...