: : www.mundoj.com.br : : Cecilia Fernandes (cecilia.fernandes@caelum.com.br): desenvolvedora, instrutora e consultora pela Caelum, cursa Ciência da Computação na USP e estagiou na IBM Research em Nova York. Entusiasta de métodos ágeis, faz palestras sobre o assunto e aplica métodos ágeis ao seu dia-a-dia, desde metodologias e práticas até ferramentas para auxiliar times ágeis, como a apresentada neste artigo. Builds com Gradle: programativo e declarativo Builds Java são feitos com Ant e Maven há anos. São duas excelentes ferramentas, cada uma com suas vantagens e desvantagens. Tentando encontrar um balanço entre as duas, surgiu o Gradle, que através da linguagem Groovy fornece uma maneira simples de trabalhar tanto declarativamente quanto programaticamente, quando necessário. Conheça a ferramenta de build para a JVM que está ganhando muita popularidade. urante o desenvolvimento de software, todo desenvolvedor precisa automatizar o build de seu projeto e mais frequentemente do que deveria, esta atividade se torna uma grande dor de cabeça ao fugirmos minimamente dos padrões preestabelecidos. As abordagens mais comuns em Java para configuração do build de projetos dividem-se entre o XML programático do Ant e o uso de Maven, mas sabemos que nenhuma delas é ideal muitas vezes, apenas convivemos com essas ferramentas por falta de opção ou, melhor dizendo, falta de opção compatível com o restante do mercado. Já era hora de trocarmos a programação de XML e os inúmeros problemas de customizações, sem mencionar o tempo desperdiçado sempre que aguardamos o Maven sincronizar com o repositório, por uma nova alternativa. O Gradle é um sistema de builds cuja configuração é feita programaticamente, usando uma elegante DSL em Groovy. Isso significa que um build feito em Gradle pode ter métodos, closures e tudo o mais que a linguagem Groovy permite. Neste artigo, você verá como configurar o build de um projeto em Java usando o Gradle, inclusive customizando diversas opções. Conforme avançamos, a própria sintaxe do Groovy será explicada, para que você seja capaz de continuar explorando o Gradle, ainda que nunca tenha programado em Groovy antes. Descubra quais vantagens fizeram com que tanto o próprio Groovy quanto o Grails e até mesmo o Hibernate estejam optando por essa nova ferramenta gratuita de build, distribuida sob a licença Apache 2.0. 40
Instalação A instalação do gradle é bastante simples: basta baixar o pacote de www.gradle.org, descompactá-lo e colocar o seu diretório bin dentro do seu path. Sua única dependência é um JRE 1.5+ instalado e configurado. Rodando gradle v deve listar as versões do gradle e de suas dependências. Primeiros exemplos À moda do build.xml do Ant, o arquivo que configura um build no Gradle chama-se build.gradle. Ele é um arquivo em Groovy, que contém a configuração do projeto através de objetos do tipo task, que o Gradle é capaz de executar. Iniciemos nosso contato com a ferramenta através de um clássico Hello World, para ilustrar nosso exemplo. Em um arquivo build. gradle localizado na raiz do projeto, escreva as seguintes linhas: task olamundo << { println 'Oi mundo!' Em seguida, no terminal, execute: > gradle olamundo -q Embora o parâmetro -q seja desnecessário, ele indica que preferimos a forma "quieta" de execução, que não informa o que exatamente está sendo feito. Contudo, se for interessante ver detalhadamente o que está sendo executado, basta rodar a mesma linha sem esse parâmetro. Tasks Gradle x Tasks Ant Se você já teve contato com o Ant, poderia pensar que as tasks do Gradle são equivalentes às tasks do Ant. Já vamos, então, desde o início, desfazer essa confusão. Trabalhando com o Ant, tasks são comandos simples que o Ant consegue executar, como echo e javac. Uma sequência de tasks do Ant é chamada de Target. Já no Gradle, chamamos de tasks o equivalente aos Ant targets, isto é, um conjunto de comandos para executar uma operação, algo que seja valioso em nosso build. E para trabalhar com Java? Seguindo o mote do Java de Convenção sobre Configuração, se seu projeto seguir algumas poucas convenções, basta uma única linha para montar um build inicial com o Gradle. Entre essas convenções, destaca-se a localização das classes de códigos de produção e de testes. Elas devem estar, respectivamente, em src/main/java e src/test/java. Similarmente, os resources de produção e de testes devem estar em pastas: src/main/resources e src/test/resources. Se você já usou Maven, certamente notou que a convenção é a mesma e isso é intencional tanto facilita a migração quanto adota um padrão, o que evita configuração desnecessária. Também é possível alterar esses valores através da configuração sourcesets. Para começar a usar a estrutura já preparada para Java, basta indicar que você usará esse plugin, adicionando ao build.gradle, necessariamente no início do arquivo, a linha abaixo. apply plugin: 'java' É necessário que essa linha esteja logo no início do arquivo, ou ao menos antes da primeira task que utilize a infraestrutura do plugin de Java, já que o Gradle, como qualquer código Groovy, interpreta o arquivo sequencialmente. Isso significa que, se chamarmos qualquer dependência do plugin de Java antes de declarar que utilizaremos esse plugin, o Gradle falhará, dizendo que não conhece tal dependência. A partir desse momento, algumas tasks e variáveis padrão passam a estar disponíveis e, num momento inicial, se seu projeto não depender de nenhuma biblioteca, já é possível chamar a criação de um build dele, rodando no terminal o comando: > gradle build A task build chama, internamente, diversas outras. Vejamos agora sem a opção -q, mais a fundo o que acontece quando executamos esse comando. Sua saída no terminal já nos provê bastante informação, indicando, linha a linha, quais outras tasks foram internamente chamadas: :compilejava :processresources :classes :jar :assemble :compiletestjava :processtestresources :testclasses :test :check :build BUILD SUCCESSFUL Acompanhe nessa lista o que é feito: o build começa compilando as classes e copiando os recursos para as localizações pertinentes. Em seguida, cria o JAR e monta a estrutura da pasta build, contendo todos os subprodutos dessas ações. As ações de verificação são, então, executadas, embora nesse particular momento não tenham efeito algum ainda, já que não temos classes de teste. Em seguida, a verificação é finalizada e o build completo se você não esqueceu de nada, o build será bemsucedido. Repare que agora temos o jar do nosso projeto, apenas seguindo as convenções já conhecidas pelos usuários do maven, e com o arquivo de configuração praticamente vazio. Nosso próximo passo é, então, adicionar classes de teste e, com elas, o JAR da biblioteca de testes o que nos leva ao próximo problema. 41
: : www.mundoj.com.br : : 42 Tratando dependências A partir do momento que temos testes, temos quase que necessariamente a adição da dependência de uma biblioteca de testes, comumente o Junit. E, mesmo sem eles, é quase regra que nossos projetos dependam de outros JARs. Dessa forma, o gerenciamento dessas dependências é assunto mais do que importante em uma ferramenta de build. O Gradle permite que resolvamos dependências buscando-as tanto em diretórios locais, como uma pasta "lib/" no projeto, quanto em repositórios Maven ou Ivy. Se usando gerenciadores de dependências, os repositórios padrão já vêm pressupostos. Para configurar as dependências do seu projeto, basta adicionar comandos ao bloco "dependencies". No exemplo da Listagem 1, pedimos que os JARs do JUnit e do Log4J sejam baixados do repositório do Maven e, além disso, que os jars no diretório "lib/" sejam registrados. Listagem 1. Configurando dependências. repositories { mavencentral() dependencies { compile group: junit, name: junit, version: 4.+ compile group: log4j, name: log4j, version: 1.2.12 lib = $projectdir/lib compile files(filetree(dir: lib as File, includes: [ *.jar ])) Dessa forma, combinamos tanto o download automático de dependências quanto o uso de bibliotecas que já estejam no nosso diretório lib. É bastante comum deixar no diretório lib as bibliotecas que não estão disponíveis em repositórios maven, como é o caso do mail.jar, ou de algum JAR interno da sua empresa. Há ainda uma sintaxe mais sucinta para declarar as dependências, como: compile ( junit:junit:4.+ ). Caso sua dependência esteja em algum outro repositório, digamos, do Maven, você pode customizar os repositórios nos quais o Gradle buscará as dependências. Essa configuração é feita complementando o bloco repositories. repositories { mavencentral() mavenrepo urls: http://repository.jboss.org/nexus/content/groups/public' Apenas configurando as dependências, o build já saberá quais JARs usar se precisar executar os testes, por exemplo. Contudo, isso não faz com que o Classpath seja configurado, ou seja, teríamos que rodar o projeto passando, em linha de comando, os JARs da aplicação. Para configurar informações que usualmente ficariam no Manifest, temos duas formas padrão. A primeira, trivial, seria exatamente criar um MANIFEST.MF e indicá-lo no build.gradle usando a linha: manifest.from("path/to/manifest.mf") A segunda maneira seria configurar o Manifest no próprio build.gradle, centralizando ali todas as configurações do projeto. Nesse caso, podemos configurar o Manifest programaticamente e ele será gerado dinamicamente. No exemplo que estamos construindo, teríamos: manifest.mainattributes("main-class": "br.com.empresa.projeto.principal", "Class-Path": "lib/xstream-1.3.jar lib/log4j-1.2.12.jar") Seja criando um MANIFEST.MF ou configurando suas informações diretamente no build.gradle, a partir do momento que essa configuração existe já é possível rodar o JAR chamando simplesmente "java -jar nomedojar". Nesse momento, rodemos novamente, no terminal, o comando: > gradle build Note, agora, que ao final da execução bem-sucedida, o Gradle criará, na raiz do projeto, a pasta build/, com uma estrutura de diretórios dentro dela. Dentro dessa pasta, o JAR da aplicação é colocado dentro da pasta libs/. Seu nome será, por padrão, no formato: NomeDoProjeto,jar. O nome do projeto vai ser o nome do diretório em que o build.gradle se encontra, ou você pode alterar esse nome setando a variável rootproject.name dentro de um novo arquivo settings.gradle. Se seu projeto for versionado, também é simples adicionar um número de versão a ele e, automaticamente, ao nome do JAR gerado. Para essa configuração, faça: version = '0.5' Dessa forma, o JAR gerado será automaticamente nomeado: NomeDoProjeto-0.5.jar. Criando distribuíveis O próximo passo natural do seu projeto provavelmente será disponibilizar um entregável zipado contendo tanto o JAR da sua aplicação quanto suas dependências e talvez alguns arquivos acessórios a mais. Com o Gradle é muito fácil fazer isso. Basta escrever uma task do tipo Zip e, a partir do momento que ela existir, o plugin do Java já tomará a iniciativa de rodá-la, criando um arquivo com o conteúdo especificado na implementação dessa task. No nosso exemplo, veja como fica a task de empacotamento, na Listagem 2. Listagem 2. Montando um entregável. task zip(type: Zip, dependson: jar) { from $builddir/libs into( lib ) { from configurations.runtime
Um pequeno detalhe sobre esse exemplo é que podemos criar uma task de nome "zip" sem o parâmetro "dependson: jar", mas isso é apenas uma gentileza que o plugin do Java nos oferece por usar o nome padrão dessa extensão. Porém, se a task tiver outro nome, é necessário especificar esse parâmetro. Caso contrário, a task poderá ser executada antes de o JAR ser montado, isto é, o Zip gerado conterá apenas as dependências, mas não o JAR da sua aplicação. Destrinchando o pequeno código da Listagem 2, estamos dizendo que é uma tarefa a ser executada e que, no diretório raiz do Zip criado, será colocado o conteúdo da pasta build/libs/, que já vimos ser o JAR da sua aplicação. Além dele, dentro de uma pasta lib/ no pacote zipado, serão colocados os JARs dos quais ela depende. Um pouco de Groovy Os métodos from e into, inerentes de tasks do tipo Zip e do tipo Copy do Gradle, têm uma forma simples, que recebe um Object indicando de ou para onde, respectivamente, vão os arquivos você pode ver essa forma sendo utilizada na primeira linha da task zip, na Listagem 2. Contudo, existe também uma sobrecarga desses métodos que recebe, além desse parâmetro, uma closure que deve conter o lado oposto dessa relação, isto é, se chamamos o from originalmente, precisamos dizer onde os arquivos devem ser colocados e, no outro caso, vice-versa. Pode-se, inclusive, adicionar mais diretórios de origem e as subpastas, usando essas closures. O Gradle cuida de montar o Zip da melhor forma possível, empacotando todos os arquivos indicados. De volta ao exemplo Adicionando a task de zip que acabamos de criar e entender ao nosso build.gradle, basta rodar o build novamente para notar a diferença: uma task zip surge entre as executadas pelo comando build. :compilejava :processresources UP-TO-DATE :classes :jar :zip :assemble :compiletestjava :processtestresources UP-TO-DATE :testclasses :test :check :build BUILD SUCCESSFUL Problemas de encoding? Configurações de encoding são pesadelos na vida de muitos programadores e, em particular, assombram os que configuram os builds de projetos. Misteriosas configurações que alteram apenas o escopo de compilação no Maven, mas não o de execução, confundem até o mais experiente programador. No Gradle, essa configuração é trivial. Basta configurar a variável encoding das opções de compilação de classes e testes e ele já assumirá que deve usar esse encoding dali em diante. Por exemplo, se o encoding adotado pela equipe é o UTF-8, adicionaremos o seguinte código ao build.gradle: Listagem 3. Mudando o encoding. options.encoding= UTF-8 compiletestjava { options.encoding= UTF-8 Ou então, abusando um pouco da sintaxe do groovy, podemos sucintamente escrever: [compilejava, compiletestjava]*.options*.encoding= UTF-8 Adicionando essas linhas, o Gradle interpretará seu código como UTF-8 na compilação das classes. Mais um pouco de Groovy Note, na Listagem 3, o código resumido para fazer o mesmo que o código com blocos acima. Essa sintaxe também causa confusão ou estranheza, comumente. O que significa, entretanto, é bastante simples. Os colchetes delimitam uma lista de dois itens, separados pela vírgula. Em seguida, vemos uma incomum construção da forma *. que significa para cada item da lista cuja instrução se aplique, faça.... Isto é, lendo a sentença como um todo temos: para cada item da lista de grupos de compilação, pegue as opções e, para cada opção que tenha a propriedade encoding atribua UTF-8 a ele. Esta é apenas uma forma com características mais funcionais de lidar com objetos. Diferenciando JARs de testes Ao fazer testes automatizados, é frequente utilizarmos JARs, como Junit e Jmock ou Mockito, que servem exclusivamente para fazer rodar esses testes, e não serão usados na distribuição ou deploy. Já que escrever testes automatizados é nada mais que obrigação de um bom programador atual, o Gradle já vem preparado para lidar com diferentes grupos de compilação. Usando esse recurso, é fácil separar os JARs necessários para o funcionamento da aplicação daqueles que são apenas usados pelos testes automatizados e que, portanto, podem ficar fora do pacote de produção. Sempre que utilizamos o plugin de Java para o gradle, já obtemos dois grupos de compilação padrão, o compile e o testcompile. Separando os JARs nesses grupos padrão, o próprio Gradle já é capaz de separar corretamente os contextos. Mudando seu código da definição de dependências para usar ambos os grupos, então, teremos o código de dependências do build. gradle alterado como mostrado na Listagem 4. 43
: : www.mundoj.com.br : : Listagem 4. Separando grupos de compilação. dependencies{ testcompile group: junit, name: junit, version: 4.+ compile group: log4j, name: log4j, version: 1.2.12 lib = $projectdir/lib compile files(filetree(dir: lib as File, includes: [ *.jar ])) Dessa forma, quando construirmos o pacote de distribuição, apenas os JARs de produção serão empacotados, mas quando rodando os testes, o JUnit será incluído. Rodando seu projeto web Caso seu projeto seja web, basta seguir as mesmas convenções do Maven, com seus JSPs e WEB-INF dentro de src/main/webapp. O gradle por enquanto possui apenas suporte nativo ao Jetty 6, e para rodá-lo e fazer o deploy da sua aplicação web, basta fazer gradle jetty Mas suponha que precisamos rodar em outro container, como o Tomcat 7. Vejamos algo um pouco mais sofisticado no Gradle. Poderíamos adicionar as dependências para que a nossa configuração compile, mas aí, quando gerássemos nossa distribuição, o tomcat 7 viria junto! Criando seu grupo de compilação Para organizar melhor, vamos criar uma configuração à parte, chamada tomcat, para guardar essas dependências: configurations { tomcat Agora, dentro de dependencies, podemos adicionar as dependências referentes ao tomcat 7. Repare que há um erro no pom.xml do tomcat 7.0.2, e por isso contornamos esse problema excluindo a dependência que foi registrada incorretamente (veja referência): Listagem 5. Configurando web container no grupo de compilação. dependencies{ // outras dependencias... tomcat ( org.apache.tomcat:tomcat-catalina:7.0.2 ) tomcat org.apache.tomcat.embed:tomcat-embed-core:7.0.2 tomcat ( org.apache.tomcat:tomcat-jasper:7.0.2 ) { exclude group: org.eclipse.jdt, name: ecj,version: 3.6 tomcat group: org.eclipse.jdt.core.compiler, name: ecj,version: 3.5.1 Vamos então criar uma task que execute o bootstrap do servidor. Nela, configuramos o Classpath para usar as dependências do Tomcat, além de aproveitar para configurar alguns parâmetros da JVM veja a Listagem 6. Listagem 6. Task para rodar o Tomcat 7. task runtomcat7(type: JavaExec, dependson: build) { classpath configurations.tomcat jvmargs -Xmx800M main = org.apache.catalina.startup.bootstrap Para rodar essa task, basta executar no terminal: gradle run- Tomcat7. Repare que agora não há uma integração clara entre o Tomcat e o Gradle, o que torna necessário um diretório conf/ dentro do seu projeto, junto com o server.xml e a configuração para que o Tomcat carregue o seu contexto de dentro do WAR que está em build/libs. Suporte ao Eclipse Assim como o Maven, o Gradle pode facilmente gerar os arquivos necessários para que seu projeto seja importado dentro do Eclipse. Basta avisar que usaremos o plugin do Eclipse, exatamente como fizemos com o de Java logo acima (apply plugin: 'eclipse') e, então, rodar: gradle eclipse Ao rodar tal comando, ele gera até mesmo os arquivos necessários para o WTP, possibilitando o start do seu servlet container através do Eclipse. É importante, no entanto, configurar uma variável de classpath GRADLE_CACHE dentro do seu Eclipse apontando para o diretório do seu usuário somado a /.gradle/cache. Excelente, mas... Por ser uma ferramenta consideravelmente nova, ainda há muitas mudanças na própria sintaxe do Gradle, a cada lançamento de uma nova versão e, com isso, por mais que a documentação atual esteja disponível é frequente que buscas tragam informações desatualizadas e que não mais correspondem à versão corrente do Gradle. Ainda há, também, algumas deficiências em mensagens de erros mais incomuns. Por exemplo, testando o que acontece se a internet cai enquanto o Gradle conversa com um repositório Maven2, a mensagem de erro descreve, não que o repositório não pode ser alcançado, mas que tal JAR não existe, o que pode ser confuso. Lentidão característica O Gradle é, sem dúvida, um salto em simplicidade, quando comparado ao Maven ou ao Ant, contudo ainda sofre sensivelmente com a vagarosidade do Groovy. Por ser uma trivial chamada a código Groovy, a cada instância o JIT não reaproveita as otimizações que podem ter ocorrido previamente. 44
Felizmente, o tempo de build de um projeto aumenta cada vez a uma taxa menor conforme a complexidade e o tamanho dele aumenta, mas ainda assim o tempo necessário para rodar é um dos pontos mais notoriamente negativos do Groovy. Para Saber Mais Na edição 42, o artigo Conhecendo as Principais Linguagens para JVM já falava de Groovy e seu uso para produzir elegantes DSLs como o Gradle. Considerações Finais Ainda que bastante recente, o Gradle é uma ferramenta promissora ao trazer grandes melhorias quando comparado aos seus predecessores, Ant e Maven. Finalmente podemos configurar builds de forma programática e declarativa, usando uma sintaxe elegante que encapsula o Ivy. O Gradle também possui plugins para compilar e trabalhar com projetos em Scala e Groovy. Outra alternativa bastante similar é o Apache Buildr (http://buildr. apache.org/), que tem a mesma proposta, mas usa sintaxe Ruby como base. Essa ferramenta também vale um teste por sua rapidez, principalmente se já houver familiaridade com a linguagem. Ela é, juntamente com o Gradle, recomendada por Jez Humble e David Farley no livro Continuous Delivery. Sua ideia perde apenas para a Simple Build Tool (SBT): outra ferramenta de build, voltada para Scala, que permite interação programática através de um shell. A ideia é que o SBT roda durante todo o tempo de desenvolvimento e, com isso, tem mais chance de ter seu script otimizado Referências Por que Gradle? Grupo JBoss responde http://community.jboss.org/wiki/gradlewhy Migrando do Maven pro Gradle http://www.beeworks.be/maven-to-gradle-part-1/ Erro no pom.xml do Tomcat 7.0.2 http://www.apacheserver.net/tomcat-7-pom-listing-wrong-ecj-dependencyat232339.htm Site do Gradle http://gradle.org/ Documentação da versão citada no artigo http://www.gradle.org/0.9-rc-1/docs/userguide/userguide.html