Construindo Múltiplos Projetos com Gradle Aprenda como criar uma estrutura múltiplos projetos, definir suas dependências e algumas dicas para customizar ainda mais a construção de um projeto. Daniel Figueiredo ([email protected]) é engenheiro de software graduado pela Universidade Tiradentes em 2009, trabalhando de São Paulo para a INFOX Tecnologia da Informação/SE. Desenvolve sistemas Web desde 2008, trabalhando com Jboss Seam, JBPM, JSF, entre outros. Automação de projetos é um ponto essencial para o sucesso na construção de softwares. Esta deve ser fácil, simples e até mesmo divertida de se implementar. Não há um tamanho único, um modelo pronto, de processo que atenderá todos os tipos de construções. Portanto o Gradle não impõe um rígido e único processo sobre as pessoas. Contudo, ele foca em encontrar e descrever o processo de construção-alvo, que é o mais importante, fornecendo toda a liberdade necessária para criar uma construção declarativa, sustentável, de forma concisa e de alta performance. Além de possibilitar a definição de estruturas fragmentadas, como o conceito de multiproject (múltiplos projetos), no qual cada projeto representa um módulo, uma camada da estrutura geral fragmentada para se obter melhor coesão, baixo acoplamento, maior organização dos serviços e funções, sendo possível acessar informações e estipular diversos tipos de dependências entre esses projetos ma ferramenta para construção de projetos sempre foi algo desejável, desde o surgimento da necessidade de compartilhá-los e padronizá-los da forma mais simples possível (apesar da complexidade de algumas arquiteturas). Essas estruturas normalmente são separadas em módulos e devem possuir vários requisitos, como, por exemplo: portabilidade de IDE, facilidade para locação das dependências, empacotamentos automáticos, construção de tarefas específicas, automatização de configurações, entre outros. Atualmente existem algumas ferramentas e utilitários que podem preencher essas diversas exigências, em uma ordem cronológica ascendente podemos citar os mais conhecidos: os clássicos scripts Ant, o Maven e o atual Gradle. O mais utilizado atualmente por diversas empresas é o Maven 2 (versão atual, pois houve uma grande mudança proveniente da primeira versão, logo, para diferenciar sem ocorrer dúvidas e problemas foi adotada essa nomenclatura). Entretanto, o Maven 2 trouxe diversos questionamentos, pois para quem já trabalha com ele, sabe que existe uma grande dependência de plug-ins para satisfazer os requisitos, além do dispendioso tempo de escrita em arquivos xml, que acabam ficando imensos dificultando a manutenção, entre outros fatores. Entretanto, o objetivo deste artigo não é fazer nenhuma crítica ao Maven, mas sim expor os pontos que o Gradle proporciona um benefício melhor sobre o assunto. O “coração” do Gradle é que ele coloca as declarações em um nível superior, pois os elementos são feitos em uma Domain Specific Lan- 59 : : www.mundoj.com.br : : guage (DSL), baseada no Groovy e podem ser montados da forma que desejar. Esses elementos também fornecem suporte build-byconvention (suporte já na convenção) para projetos Java, Groovy, OSGi, Web e Scala. O Gradle também permite a aplicação de diversos princípios de design no código da construção, afinal é muito fácil compor peças reutilizáveis de lógica, assim como definir múltiplos projetos (multiproject), gerenciar de diversas formas as estratégias de dependências (é possível acessar repositórios Maven, Ivy e repositórios locais) e migrar para o Gradle (pois ele se adapta a qualquer estrutura). Gradle é um projeto de código aberto (open-source) e é licenciado sob a ASL (veja nas referências). Neste artigo será apresentado o exemplo que explicará como construir um projeto com o Gradle que possui subprojetos, assim como a utilização do GUI provido pela própria ferramenta, entre outras coisas. Para melhor entendimento do conteúdo proposto, é interessante ter em mãos a edição 44 da MundoJ, pois ela possui um artigo de Cecília Fernandes que abordou o mesmo tema "Gradle Construtivo e Declarativo", demonstrando como criar tasks, definir dependências externas, gerar jars com o código-fonte da aplicação, configurar o encoding, utilizar o plug-in jetty para subir uma aplicação Web, inserir os arquivos do Eclipse no projeto e também discorreu a respeito de algumas vantagens e desvantagens analisadas. Sendo assim, partiremos do pressuposto que o leitor já saiba como realizar determinadas funções com o Gradle, no entanto sempre que possível será explicado algumas coisas novamente para facilitar a compreensão. Criando um projeto Java O Gradle utiliza como base um arquivo chamado build.gradle (relativo ao pom.xml do Maven 2), que é escrito em Groovy, esse arquivo deve ficar na raíz do projeto (por padrão) e o Gradle irá procurá-lo dentro do diretório corrente. É neste arquivo que iremos definir as dependências do projeto, os plug-ins que serão utilizados, as tarefas, variáveis e qualquer outra coisa que seja necessária para a construção do mesmo. Partindo de uma estrutura padrão para projetos Java, observe o projeto MyModel: Listagem 1. build.gradle apply plugin: 'java' version = '1.0' jar.baseName='my-model' manifest.mainAttributes( 'Implementation-Title': 'My Model', 'Implementation-Version': version ) fará com que diversas tarefas sejam adicionadas ao projeto, como veremos a seguir. Na segunda, foi atribuído o valor '1.0' ao atributo version, esse, por padrão, já pertence ao projeto. Em seguida (linha 3) foi definido o jar.baseName, que significa o nome padrão que será utilizado a esse projeto caso seja empacotado como um arquivo .jar. Após a quarta linha são declaradas as informações do arquivo MANIFEST que será incluído no projeto sempre que esse for construído (não se preocupe em tentar entender a origem dos valores e atributos, o User Guide do Gradle referenciado vai ajudá-lo perfeitamente a compreender como as instâncias dos projetos são criadas e funcionam mais detalhadamente, enquanto este artigo o fará entender, mesmo que resumidamente, como utilizar e criar rapidamente uma aplicação). Utilizando o GUI do Gradle Para esse exemplo foi adotada a utilização do GUI do Gradle, ao invés das linhas de comando, pois para didática fica mais fácil o entendimento e nos permitirá uma visualização melhor das tarefas que o projeto contém. Para abrir o GUI, basta digitar o comando: Comando: "gradle -gui" Figura 2. Linha de comando necessária para abrir a interface do Gradle. Se tudo foi instalado corretamente, isso abrirá uma tela similar a essa: Figura 1. Estrutura do projeto MyModel. Por padrão o Gradle espera encontrar o código-fonte de produção em src/main/java e o de testes em src/test/java (neste artigo será utilizado o padrão para facilitar o aprendizado do conceito. Porém, é possível informar diretórios adicionais a esse para localização dos códigos-fontes, tanto de teste como produção, mais informações podem ser obtidas nas referências). Sendo assim, a pasta src/main/ resources será incluída no arquivo JAR como resources e quaisquer arquivos contidos dentro do mesmo diretório serão incluídos no classpath usado para execução dos testes. Agora é necessário criar o arquivo build.gradle na pasta raíz, afinal é preciso fazer com que o Gradle reconheça o projeto: Listagem 1: na primeira linha é informado o plug-in que iremos utilizar, no caso 'java', porque nosso projeto é um projeto java e isso Figura 3. GUI do Gradle exibindo o projeto MyModel e suas possíveis tarefas. 60 No entanto, para visualizar o projeto criado vá até a aba Setup -> Browse... e defina o Current Directory (diretório corrente) como a pasta raiz, no caso MyModel. Automaticamente o Gradle já irá realizar a busca no diretório para encontrar o arquivo build.gradle criado e se tudo ocorrer bem, ele irá exibir a árvore de tasks do projeto. Clicar duas vezes na tarefa fará com que ela seja executada. Sendo assim execute a tarefa build e abra o diretório-raiz do projeto, note que uma pasta "build" e ".gradle" foram criadas. A primeira contém o .jar do projeto que é gerado junto com resultados dos testes e a segunda é para armazenar cache e algumas propriedades do próprio Gradle. Observe também que, se for executado novamente um build sem alterar nada no projeto, deverá ser exibido um log parecido com esse: Verifique que dessa vez é utilizado o plug-in 'war', afinal é um projeto Web e por coincidência (ou não) este projeto será empacotado nesse mesmo formato. No entanto, ainda não foram definidas as dependências. Sendo assim, adicione a seguinte linha de código em cada arquivo build.gradle, modificando apenas a string ':NomeProjeto' pelo nome do projeto que ele realmente depende, no caso de MyService, ":MyModel" e no de MyWeb, ":MyService" (não se esqueça dos dois pontos antes do nome), como especificado no início do capítulo: build.gradle dependencies { compile project(':NomeProjeto') } Listagem 2. Executing command: ":build" :compileJava UP-TO-DATE :processResources UP-TO-DATE :classes UP-TO-DATE :jar UP-TO-DATE [...] mais código BUILD SUCCESSFUL Total time: 2.751 secs Os três projetos foram criados e a dependência entre eles foi definida, mas ainda assim o Gradle não é capaz de resolver essa hierarquia, devido a falta de um arquivo de configuração em um projeto root (raíz) que irá englobar todos os demais já criados (para os familiarizados com Maven 2, isso não deve ser novidade). Logo, crie um diretório Root e coloque os três projetos dentro: Listagem 2: isso ocorre porque o Gradle sabe que não houve nenhuma modificação com o código. Sendo assim, tudo já está UP-TO-DATE atualizado, não precisando ser recompilado. Múltiplos projetos e dependências Após a criação do projeto MyModel, é necessário criar ao menos mais um projeto para a construção de um esquema multprojects. Sendo assim imagine o seguinte cenário, MyModel <- MyService <-MyWeb, em que "<-" indica a dependência entre os projetos. Conforme já feito anteriormente, o projeto MyService irá ser construído na estrutura padrão de um projeto Java e o MyWeb na estrutura padrão de um projeto JavaWeb: Figura 4. Estrutura do projeto MyService e seu arquivo build.gradle. Figura 6. Estrutura dos projetos dentro da raíz (Root). Em seguida, dentro da pasta Root crie dois arquivos, respectivamente chamados: "build.gradle" e "settings.gradle", com os respectivos códigos: Listagem 3. build.gradle allprojects { repositories { mavenLocal() mavenCentral() } } Listagem 3: nesse arquivo "build.gradle" está definido que para todos os projetos (allprojects { }) serão utilizados os repositórios contidos dentro da tag "repositories { ... }", no caso, apenas o cache local e o repositório central do Maven. Também existe a possibilidade de acrescentar outros repositórios, como a seguinte linha de comando abaixo em negrito, que acrescenta o repositório da Jboss: mavenRepo urls: "http://repository.jboss.com/maven2" Agora é preciso informar quais são esses projetos que o root deve procurar, para isso coloque no arquivo settings.gradle criado, a seguinte linha de código: Figura 5. Estrutura do projeto MyWeb e seu arquivo build.gradle. Listagem 4. setting.gradle include "MyModel", "MyService", "MyWeb" 61 : : www.mundoj.com.br : : Listagem 4: inclusão dos três projetos criados, o que irá permitir que as dependências entre eles, assim como acessar "tasks" ou informações contidas nos outros projetos. Abra novamente o GUI do Gradle e selecione dessa vez a pasta Root, algo similar a essa tela deve aparecer: Listagem 6. build.gradle task cleanWarExploded(dependsOn: clean) { ant.delete(dir: 'diretorioWarExploded') } Listagem 6: novamente é utilizada uma função ant, dessa vez para deletar um diretório/arquivo informado, como exibido na linha 2. Note ainda que essa nova task depende da task clean. Sendo assim, ao executá-la, o diretório build também será excluído, pois a task clean será chamada antes que a task cleanWarExploded seja realizada, como pode ser analisado na linha 1. Adicionando os Projetos a uma IDE Figura 7. GUI do Gradle exibindo a pasta Root com seus projetos. Caso exista a necessidade de gerar um diretório exploded a partir do WAR criado incluindo os jars dos projetos Java, analise a se seguinte linha de comando: build.gradle Listagem 5. war.doLast{ ant.unzip(src:project(':MyWeb').war.archivePath, dest: new java.io.File( 'insiraOCaminhoDoDiretorio')) } Listagem 5: observe que foi utilizado um método ant, nesse é necessário informar o diretório (arquivo) de origem que irá ser extraído e um diretório destino onde será colocado os registros obtidos na extração. O Gradle possui um padrão para as tarefas onde se pode executar códigos em dois momentos distintos, um antes da tarefa a ser executada e um depois. Sendo assim, sempre que necessário inserir códigos em algum desses momentos defina um bloco doFirst {...} ou doLast{...} dentro da task desejada. Uma sintaxe alternativa para o código acima é: war { doLast{ ant.unzip(src:project(':MyWeb').war.archivePath, dest: new java.io.File('caminhoDoDiretorio')) } } Até agora foi visto como definir uma estrutura, habilitar os plugins para que os projetos sejam empacotados em jars, wars, entre outras formas possíveis com outros plug-ins. No entanto, ainda não é possível adicionar o projeto a uma IDE (é claro que os de desenvolvedores mais experientes já sabem quais arquivos devem criar para que uma IDE como Eclipse, ou NetBeans o reconheça), mas não precisamos ter esse trabalho braçal, vejamos como (na edição 44 já foi exibido como realizar essa função, porém aqui é definido no projeto Root para habilitar esse plugin a todos os projetos): ar 1. Adicione as duas linhas de código a tag "allprojects {} "do arquivo "build.gradle" do projeto root: Listagem 7. allprojects { repositories { mavenLocal() mavenCentral() } apply plugin: 'eclipse' } Listagem 7: novamente adicionamos funcionalidades comuns a todos os projetos, nas linhas 6 e 7 foram acrescentados dois plug-ins, um que fará a automatização desse projeto para o padrão Eclipse, incluindo no .classpath as dependências declaradas. É possível gerar os arquivos para que o projeto também seja reconhecido pelo NetBeans e IntelliJ, basta olhar a seção de plug-ins no manual de referência, este tutorial irá utilizar o Eclipse, por ser a IDE padrão adotada. 2. Atualize o GUI do Gradle e deverá ser possível visualizar as novas tasks (tarefas) dentro dos projetos, execute a task de nome "eclipse" ao menos uma vez para cada projeto (incluindo o Root). Note que arquivos serão gerados nas pastas do projeto, como podemos ver na imagem a seguir: Seria interessante que a task clean, utilizada para excluir a pasta build (que é criada automaticamente quando se executa a task build) também apagasse o diretório do war exploded. É bem sim simples, acrescente o seguindo trecho de código ao build.gradle do projeto MyWeb: Figura 8. Arquivos gerados pelo plug-in do eclipse: .classpath e .project. 62 Lembre-se que, qualquer alteração nas dependências do Gradle, será necessário atualizar (ou gerar) novamente os arquivos que criados para reconhecimento do Eclipse, como foi feito acima. Fatjars e avaliação de dependências Voltando ao exemplo, imagine a necessidade de obter os dois jars, de MyModel e MyService em um único jar, termo conhecido como fatjar. É claro que neste caso, não faria sentido unir os dois conteúdos, afinal eles separam duas camadas. Contudo, se esse projeto fosse uma parte de outro maior e existissem outros projetos Service ou Model, poderia ser interessante distribuí-los em um único jar, ao invés de MyService, YourService, OurService e quaisquer outros que possam surgir. Como isso pode ser feito com o Gradle? A resposta é que existem diversas maneiras. Este artigo irá exibir uma que utiliza o compreendimento dos conceitos vistos na seção anterior. Sendo assim, considere o seguinte trecho de código que deverá ser acrescentado ao build.gradle do projeto MyService: Listagem 8. Figura 9. Projetos incluídos no workspace do Eclipse, observe que o projeto MyWeb por utilizar o plugin war já foi criado como um projeto Web Java para o Eclipse. O ciclo de vida O núcleo do Gradle é uma linguagem de programação baseada em tratar dependências, em termos mais preciso isso significa que pode ser definida tasks e dependência entre tasks. O Gradle irá garantir que essas tarefas sejam executadas, em ordem de suas dependências, sendo realizada apenas uma tarefa por vez. Logo, ele irá efetuar o gráfico total de dependências antes de que alguma task seja executada. Sendo assim, é importante conhecer as três etapas do ciclo de vida, pois conforme a dependência definida será necessário dizer ao Gradle algumas coisas, como será visto no exemplo a seguir. O ciclo de vida é dividido em três partes: 1. Inicialização Nesta etapa, o Gradle determinará quais projetos irão fazer parte da construção e criará uma instância Project para cada um desses projetos. Ele também irá procurar por um arquivo settings.gradle no diretório "master", no caso deste artigo, o projeto "root". Se não for encontrado, o Gradle será executado como um projeto único, ao invés de um projeto multiproject, é claro que além de encontrar o arquivo settings.gradle, é necessário que ele defina a hierarquia, como foi feito no capítulo 4 declarando a inclusão os projetos. 2. Configuração Todos os scripts de construção de todos os projetos serão executados, o que irá configurar os objetos Project (criados na inicialização). Para um projeto singular, o fluxo depois a inicialização é bem simples, os scripts são executados contra os objetos Project que foram criados na fase de inicialização, atribuindo assim os devidos valores. 3. Execução Determina o subconjunto de funções, criado e configurado durante a fase de configuração, a ser executado. No caso da execução de tarefas em específico, o Gradle irá procurar as tarefas com os nomes iguais aqueles passados nos argumentos da linha de comando (ex: gradle task1 task2), se esses nomes existirem, as tarefas serão executadas em uma construção separada, na ordem informada (no caso de vários argumentos). build.gradle evaluationDependsOn(':MyModel') jar { doFirst { ant.unzip(src: project(':MyModel').jar.archivePath, dest: project(':MyService').buildDir.name+'\\'+jar.baseName) } from project(':MyService').buildDir.name+'\\'+jar.baseName+'\\' doLast { File f = new File(project(':MyService').buildDir. absolutePath+'\\'+jar.baseName) if(f.exists()) { delete(f) } } } Listagem 8: esse trecho de código diz muita coisa. Na primeira linha é declarado que a avaliação desse build.gradle depende do projeto MyModel, como foi visto anteriormente no ciclo de vida, os "build scripts" (scripts de construção, os códigos do build.gradle) são avaliados sequencialmente, o que significa que os scripts avaliados antes não podem ver a configuração dos scripts que serão avaliados depois, uma maneira de controlar isso é definindo o parâmetro na linha 1. Cada projeto só pode depender da avaliação de apenas outro projeto, adicionar duas chamadas a "evaluationDependsOn('')" fará com que nenhuma seja de fato executada. Após definir a dependência de avaliação do projeto MyService, foram adicionadas e redefinidas algumas coisas na hora de execução do jar. Lembrando o objetivo de se obter um fatjar, no bloco "doFirst{}" é utilizado novamente a função ant, para extrair os dados. Desta vez é informado em um parâmetro, uma função dos projetos que usam o plugin java, "jar.archivePath" do projeto MyModel, como pode ser visto na linha 5 e no outro, o diretório build do projeto MyService concatenado com o baseName do jar definido anteriormente (capítulo 4), como pode-se observar nas linhas 6 e 7. Isso fará com que seja criada uma pasta 'my-service' dentro do diretório build do projeto MyService, contendo todo o códigofonte do jar obtido no projeto MyModel. Agora é preciso informar 63 : : www.mundoj.com.br : : que esse diretório também deve ser utilizado para a criação do jar my-service, como pode ser visto na linha 9. Por fim, para que nenhum diretório temporário permaneça no disco, é realizada uma exclusão do mesmo, como pode ser analisado dentro do bloco "doLast{}", onde é instanciado um file e após confirmada a existência do mesmo, é chamado um método do provido pelo próprio Gradle para remover diretórios com subpastas e quaisquer outras coisas. É claro que alguns ajustes terão que ser feitos para que o war exploded (capitulo 4) não fique com jars centendo classes repetitivas no diretório lib, dentro de WEBINF, no arquivo build.gradle do projeto MyWeb acrescente dentro do bloco "doLast{}" criado no bloco "war{}" após a chamada ao método ant, o seguinte trecho: Listagem 9. build.gradle war.doLast{ ant.unzip(src:project(':MyWeb').war.archivePath, dest: new java.io.File( 'insiraOCaminhoDoDiretorio')) FileTree tree = fileTree('insiraOCaminhoDoDiretorio'+'\\WEB-INF\\lib\\') for(File f : tree) { if(f.getName().startsWith(project(':MyModel').jar.baseName) { f.delete() break; } } } Listagem 9: como pode ser verificado na linha 4, outra função provida "fileTree()" é utilizada para obter toda a estrutura do diretório informado, afinal o objetivo é excluir o jar my-model, pois manualmente ele já foi colocado dentro do jar my-service, mas o Gradle não sabe disso. Sendo assim, é executado um loop para obter todos os elementos do diretório e verificar se algum deles começa com o nome definido como o baseName do jar no projeto MyModel, como mostra a linha 7, se começar, esse arquivo é removido e fim do loop. Referências 64 4JU 4JUFPmDJBMEP(SBEMFXXXHSBEMFPSH PmD .BOVBMEFSFGFSÐODJBEP(SBEMFIUUQXXXHSBEMFPSHEPDTVTFS HVJEFVTFSHVJEF@TJOHMFIUNM %4- o 1SPKFDU IUUQXXXHSBEMFPSHEPDTETMPSHHSBEMFBQJ1SP KFDUIUNM %4- o +BS IUUQHSBEMFPSHEPDTETMPSHHSBEMFBQJUBTLTCVO EMJOH+BSIUNM %4- o 8"3 IUUQHSBEMFPSHEPDTETMPSHHSBEMFBQJUBTLTCVO EMJOH8BSIUNM "4-oIUUQXXXHSBEMFPSHMJDFOTFIUNM (SPPWZoIUUQHSPPWZDPEFIBVTPSH )BOT %PDLUFS QSPmMF o IUUQTLJMMTNBUUFSDPNFYQFSUQSPmMFKBWBKFF IBOTEPDLUFS )BOT %PDLUFS 4FNJOBS o IUUQWJNFPDPNTFBSDIWJEFPT TFBSDIHSBEMFB Para Saber Mais Consulte a 44ª edição da MundoJ, ela fornecerá todas as informações básicas necessárias que complementam este artigo. Consideração finais Este artigo, de uma maneira geral, demonstrou como criar um exemplo de projeto de construção (build) com estrutura multiprojects, definindo um projeto Root, onde se situa o arquivo settings.gradle, que inclui os outros projetos na hierarquia. Possiblitando definir plugins, tasks, repositórios e quaisquer coisas comuns a todos os projetos. Com isto, foi abordado como definir dependências entre os projetos, assim como adicioná-los a uma IDE, criar um diretório exploded para o WAR e remover arquivos desse diretório de uma maneira mais fácil. Além disso, foi explicado como funciona as etapas do ciclo de vida do Gradle, fundamental para o entendimento da função que avalia as dependências entre os projetos, possibilitando a criação de um fatjar (lembrando que todos os literais strings para separação do caminho dos diretórios devem ser substituídos de acordo com o S.O., ou então utilizar a variável separator de java.io.File). Junto a isso, algumas novas funções nativas do gradle também foram utilizadas e explicadas, o que leva este artigo a conclusão de sua finalidade. Sendo assim, as informações contidas nos build scripts são partes de objetos ricos, que sabem onde devem ser copiados determinados arquivos, como realizar certas tarefas, como definir os padrões de nomenclaturas com versões, entre outras funções. Sobretudo o desempenho é otimizado, afinal o Gradle sabe quais alterações ocorreram e efetua novamente o build (ao invés de utilizar o cache) somente dos projetos necessários. Logo, por se basear em uma linguagem O.O. (Orientada a Objetos) e, consequentemente, possuir características e comportamentos, Hans Dockter, fundador e líder do projeto, comenta em um seminário na JFrog, enquanto fala sobre o projeto, que para o Gradle é necessário definir apenas o What (o que) e ele definirá o How (como). Em suma, esta é uma ferramenta muito poderosa, muitas de suas funções e capacidades não estão descritas aqui (como criação de plug-ins, dentre outros), mas já é possível entender que ele atende a qualquer regra de construção necessária. O que o destaca dos demais pela facilidade de customização, entendimento e reutilização de código, afinal escrever scripts para construção de um projeto na mesma linguagem em que ele é feito, ou em uma linguagem extensiva como Groovy, é uma grande vantagem. GUJ – Discussões sobre o tema do artigo e assuntos relacionados Discuta este artigo com 100 mil outros desenvolvedores em www.guj.com.br/MundoJ