Código dinâmico com Groovy AST Transformations

Neste post, vamos explicar como gerar código em tempo de compilação em Groovy usando AST Transformations.

Nosso objetivo será criar a anotação @JPAEntity para gerar alguns boilerplates que sempre fazemos ao criar entidades JPA.

O que são AST Transformations?

Antes de transformar código fonte em byte codes, o compilador Groovy nos fornece alguns hooks para que possamos gerar código customizado.

Esses hooks são implementações da classe org.codehaus.groovy.transform.ASTTransformation.

Ao criar uma implementação, deve-se também criar a anotação que marcará os trechos de código que serão invocados na compilação.

No nosso exemplo, criaremos a classe JPAEntityASTTransformation, responsável por gerar o código boilerplate, e a anotação JPAEntity, que usaremos para marcar as classes que queremos manipular.

Exemplos de Transformations do próprio Groovy

O próprio Groovy já possui algumas anotações que são processadas por AST Transformations.

  • @ToString: adiciona um método toString() à classe anotada
  • @EqualsAndHashCode: adiciona equals() e hashCode()
  • @Log: adiciona uma instância de log
  • @Immutable: faz com que um bean seja imutável

A lista completa você pode ver no site oficial.

Criando a anotação @JPAEntity

É uma anotação como qualquer outra. A unica coisa que devemos fazer diferente é anotá-la com @GroovyASTTransformationClass, especificando qual classe vai processá-la.

Criando JPAEntityASTTransformation

Criar uma ASTTransformation envolve implementar o método
void visit(ASTNode[] nodes, SourceUnit sourceUnit). Você recebe a árvore de compilação ASTNode, que é uma representação estrutural do seu código fonte. Nosso objetivo é adicionar a ela novos nós que representam novos trechos de código.

Existem 4 formas de manipular esta árvore. Nós vamos utilizar a que considero mais simples: escrever um código fonte em uma String, transformá-lo em nós ASTNode e adicioná-los à árvore original.

Consulte as próprias implementações de ASTTransformation do Groovy para ver outras formas de manipulação.

1. Esqueleto da classe

  • Por comodidade, estendemos AbstractASTTransformation em vez de implementarmos diretamente ASTTransformation; ela nos fornece alguns métodos convenientes.
  • @CompileStatic melhora o desempenho do transformador na compilação.
  • phase: Na fase CANONICALIZATION, a árvore AST completa e está sendo pós-processada. Em geral, usamos esta fase. Para mais detalhes, veja a documentação oficial.

2. Gerando código boilerplate a partir de uma String

Aqui, escrevemos todo o código que queremos adicionar à nossa entidade anotada com JPAEntity.

Para isso, usamos new AstBuilder().buildFromString() para transformar nosso código String em uma árvore AST.

A classe AstBuilder possui outros métodos, como buildFromCode(), que em vez de uma string, recebe uma closure.

3. Modificando a árvore AST

Por fim, devemos adicionar à árvore original os nós gerados na etapa anterior.
Esta parte é bem simples e envolve apenas manipular algumas listas.

A seguir, temos o código final:

Utilizando a Transformation

Agora basta anotarmos qualquer classe com @JPAEntity e ela se torna uma entidade com:

  • id gerado automaticamente
  • equals e hashCode implementados a partir do id
  • @Entity usando full qualified name da classe
  • @Table usando o nome simples da classe mais um número aleatório de 6 dígitos
  • implementação de Serializable

Teste unitário

Ao escrever uma ASTTransformation, é importante termos uma maneira de testá-la rapidamente. Para isso, vamos criar um teste unitário e invocar manualmente o compilador Groovy.

Para escrever o teste, usaremos o Spock, uma biblioteca BDD. Não vamos nos aprofundar muito nela. Isso será assunto para um próximo post.

Por ora, adicione-a como dependência maven

O modelo BDD não usa Teste como terminologia. Prefere o termo Specification. No fundo é a mesma coisa.

Nossa especificação (ou teste) fica assim:

Conclusão

AST Transformations são uma forma relativamente simples e poderosa de deixar nosso código Groovy mais limpo e fácil de entender. Permite-nos também reaproveitar código de maneira transparente e flexível.

É preciso sempre tomar cuidado para não criarmos caixas mágicas, que geram muitos códigos escondidos que o desenvolvedor não vê.

Aqui na Touch temos usado ASTTransformations para diminuir o boilerplate na geração de testes unitários.
O exemplo deste artigo foi retirado de uma de nossas bibliotecas internas.

O código fonte pode ser acessado no nosso GitHub.

Gostou? Não se esqueça de Recomendar e Compartilhar!