Tutorial do Bazel: criar um projeto Java

Informar um problema Acessar fonte

Este tutorial aborda os conceitos básicos da criação de aplicativos Java com o Bazel. Você configurará seu espaço de trabalho e criará um projeto Java simples que ilustra os principais conceitos do Bazel, como destinos e arquivos BUILD.

Tempo estimado de conclusão: 30 minutos.

Conteúdo

Neste tutorial, você vai aprender a:

  • Criar um destino
  • Visualizar as dependências do projeto
  • Dividir o projeto em vários destinos e pacotes
  • Controlar a visibilidade desejada em vários pacotes
  • Destinos de referência por meio de rótulos
  • Implantar um destino

Antes de começar

Instalar o Bazel

Para se preparar para o tutorial, primeiro instale o Bazel se você ainda não o tiver instalado.

Instalar o JDK

  1. Instale o Java JDK (a versão preferencial é 11, mas há suporte para as versões entre 8 e 15).

  2. Defina a variável de ambiente JAVA_HOME para apontar para o JDK.

    • No Linux/macOS:

      export JAVA_HOME="$(dirname $(dirname $(realpath $(which javac))))"
      
    • No Windows:

      1. Abra o Painel de Controle.
      2. Acesse "Sistema e segurança" > "Sistema" > "Configurações avançadas do sistema" > guia "Avançado" > "Variáveis de ambiente..." .
      3. Na lista "Variáveis de usuário" (na parte superior), clique em "Nova...".
      4. No campo "Nome da variável", insira JAVA_HOME.
      5. Clique em "Procurar no diretório...".
      6. Navegue até o diretório JDK (por exemplo, C:\Program Files\Java\jdk1.8.0_152).
      7. Clique em "OK" em todas as janelas da caixa de diálogo.

Acessar o projeto de amostra

Recupere o projeto de amostra do repositório GitHub do Bazel:

git clone https://github.com/bazelbuild/examples

O projeto de amostra deste tutorial está no diretório examples/java-tutorial e está estruturado da seguinte maneira:

java-tutorial
├── BUILD
├── src
│   └── main
│       └── java
│           └── com
│               └── example
│                   ├── cmdline
│                   │   ├── BUILD
│                   │   └── Runner.java
│                   ├── Greeting.java
│                   └── ProjectRunner.java
└── WORKSPACE

Criação com o Bazel

Configurar o espaço de trabalho

Antes de criar um projeto, você precisa configurar o espaço de trabalho dele. Um espaço de trabalho é um diretório que contém os arquivos de origem do seu projeto e as saídas de build do Bazel. Ela também contém arquivos que o Bazel reconhece como especiais:

  • O arquivo WORKSPACE, que identifica o diretório e o conteúdo dele como um espaço de trabalho do Bazel e reside na raiz da estrutura de diretórios do projeto.

  • Um ou mais arquivos BUILD, que informam ao Bazel como criar diferentes partes do projeto. Um diretório no espaço de trabalho que contém um arquivo BUILD é um pacote. Você vai aprender sobre pacotes posteriormente neste tutorial.)

Para designar um diretório como espaço de trabalho do Bazel, crie um arquivo vazio chamado WORKSPACE nesse diretório.

Quando o Bazel cria o projeto, todas as entradas e dependências precisam estar no mesmo espaço de trabalho. Os arquivos que estão em espaços de trabalho diferentes são independentes entre si, a menos que sejam vinculados, o que está além do escopo deste tutorial.

Noções básicas sobre o arquivo BUILD

Um arquivo BUILD contém vários tipos diferentes de instruções para o Bazel. O tipo mais importante é a regra de build, que informa ao Bazel como criar as saídas desejadas, como binários ou bibliotecas executáveis. Cada instância de uma regra de build no arquivo BUILD é chamada de destino e aponta para um conjunto específico de arquivos de origem e dependências. Um destino também pode apontar para outros destinos.

Dê uma olhada no arquivo java-tutorial/BUILD:

java_binary(
    name = "ProjectRunner",
    srcs = glob(["src/main/java/com/example/*.java"]),
)

No nosso exemplo, o destino ProjectRunner instancia a regra java_binary integrada do Bazel. A regra diz ao Bazel para criar um arquivo .jar e um script de shell de wrapper (ambos nomeados após o destino).

Os atributos no destino declaram explicitamente as dependências e opções dele. Embora o atributo name seja obrigatório, muitos são opcionais. Por exemplo, no destino da regra ProjectRunner, name é o nome do destino, srcs especifica os arquivos de origem que o Bazel usa para criar o destino e main_class especifica a classe que contém o método principal. Talvez você tenha notado que nosso exemplo usa glob para transmitir um conjunto de arquivos de origem para o Bazel, em vez de listá-los um por um.

Criar o projeto

Para criar seu projeto de amostra, navegue até o diretório java-tutorial e execute:

bazel build //:ProjectRunner

No rótulo de destino, a parte // é o local do arquivo BUILD em relação à raiz do espaço de trabalho (nesse caso, a própria raiz), e ProjectRunner é o nome de destino no arquivo BUILD. Você aprenderá sobre os rótulos de destino com mais detalhes no final deste tutorial.

O Bazel produz uma saída semelhante a esta:

   INFO: Found 1 target...
   Target //:ProjectRunner up-to-date:
      bazel-bin/ProjectRunner.jar
      bazel-bin/ProjectRunner
   INFO: Elapsed time: 1.021s, Critical Path: 0.83s

Parabéns, você acabou de criar seu primeiro destino do Bazel. O Bazel coloca saídas de build no diretório bazel-bin na raiz do espaço de trabalho. Navegue pelo conteúdo para ter uma ideia da estrutura de saída do Bazel.

Agora, teste seu binário recém-criado:

bazel-bin/ProjectRunner

Analisar o gráfico de dependências

O Bazel exige que as dependências de build sejam declaradas explicitamente nos arquivos BUILD. O Bazel usa essas instruções para criar o gráfico de dependências do projeto, o que permite builds incrementais precisos.

Para visualizar as dependências do projeto de exemplo, gere uma representação em texto do gráfico de dependências executando este comando na raiz do espaço de trabalho:

bazel query  --notool_deps --noimplicit_deps "deps(//:ProjectRunner)" --output graph

O comando acima diz ao Bazel para procurar todas as dependências do destino //:ProjectRunner (excluindo dependências implícitas e de host) e formatar a saída como um gráfico.

Depois, cole o texto no GraphViz.

Como você pode notar, o projeto tem um único destino que cria dois arquivos de origem sem dependências extras:

Gráfico de dependência do "ProjectRunner" de destino

Depois de configurar o espaço de trabalho, criar o projeto e examinar as dependências, adicione um pouco de complexidade.

Refinar o build do Bazel

Embora um único destino seja suficiente para projetos pequenos, divida projetos maiores em vários destinos e pacotes para permitir builds incrementais rápidos (ou seja, recriar apenas o que mudou) e acelerar os builds criando várias partes de um projeto de uma só vez.

Especificar vários destinos de build

É possível dividir o build do projeto de exemplo em dois destinos. Substitua o conteúdo do arquivo java-tutorial/BUILD pelo seguinte:

java_binary(
    name = "ProjectRunner",
    srcs = ["src/main/java/com/example/ProjectRunner.java"],
    main_class = "com.example.ProjectRunner",
    deps = [":greeter"],
)

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
)

Com essa configuração, o Bazel primeiro cria a biblioteca greeter e, em seguida, o binário ProjectRunner. O atributo deps em java_binary informa ao Bazel que a biblioteca greeter é necessária para criar o binário ProjectRunner.

Para criar a nova versão do projeto, execute o seguinte comando:

bazel build //:ProjectRunner

O Bazel produz uma saída semelhante a esta:

INFO: Found 1 target...
Target //:ProjectRunner up-to-date:
  bazel-bin/ProjectRunner.jar
  bazel-bin/ProjectRunner
INFO: Elapsed time: 2.454s, Critical Path: 1.58s

Agora, teste seu binário recém-criado:

bazel-bin/ProjectRunner

Se você modificar ProjectRunner.java e recompilar o projeto, o Bazel apenas recompilará esse arquivo.

Observando o gráfico de dependências, você verá que ProjectRunner depende das mesmas entradas que antes, mas a estrutura do build é diferente:

Gráfico de dependência do "ProjectRunner" de destino após adicionar uma dependência

Você acabou de criar o projeto com dois destinos. O destino ProjectRunner cria dois arquivos de origem e depende de outro destino (:greeter), que cria um arquivo de origem extra.

Usar vários pacotes

Agora, vamos dividir o projeto em vários pacotes. Se você observar o diretório src/main/java/com/example/cmdline, vai notar que ele também contém um arquivo BUILD, além de alguns arquivos de origem. Portanto, para o Bazel, o espaço de trabalho agora contém dois pacotes, //src/main/java/com/example/cmdline e // (já que há um arquivo BUILD na raiz do espaço de trabalho).

Dê uma olhada no arquivo src/main/java/com/example/cmdline/BUILD:

java_binary(
    name = "runner",
    srcs = ["Runner.java"],
    main_class = "com.example.cmdline.Runner",
    deps = ["//:greeter"],
)

O destino runner depende do destino greeter no pacote // (por isso, o rótulo de destino //:greeter). O Bazel sabe isso pelo atributo deps. Confira o gráfico de dependências:

Gráfico de dependência do "executor" de destino.

No entanto, para que o build tenha sucesso, é necessário conceder explicitamente o destino runner na visibilidade //src/main/java/com/example/cmdline/BUILD para os destinos em //BUILD usando o atributo visibility. Isso ocorre porque, por padrão, os destinos estão visíveis apenas para outros destinos no mesmo arquivo BUILD. O Bazel usa a visibilidade de destino para evitar problemas, como bibliotecas com detalhes de implementação que vazam para APIs públicas.

Para fazer isso, adicione o atributo visibility ao destino greeter no java-tutorial/BUILD, conforme mostrado abaixo:

java_library(
    name = "greeter",
    srcs = ["src/main/java/com/example/Greeting.java"],
    visibility = ["//src/main/java/com/example/cmdline:__pkg__"],
)

Agora é possível criar o novo pacote executando o seguinte comando na raiz do espaço de trabalho:

bazel build //src/main/java/com/example/cmdline:runner

O Bazel produz uma saída semelhante a esta:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner.jar
  bazel-bin/src/main/java/com/example/cmdline/runner
  INFO: Elapsed time: 1.576s, Critical Path: 0.81s

Agora, teste seu binário recém-criado:

./bazel-bin/src/main/java/com/example/cmdline/runner

Você modificou o projeto para criar dois pacotes, cada um contendo um destino, e entendeu as dependências entre eles.

Usar rótulos para fazer referência a destinos

Em arquivos BUILD e na linha de comando, o Bazel usa rótulos de destino para fazer referência a destinos, por exemplo, //:ProjectRunner ou //src/main/java/com/example/cmdline:runner. A sintaxe deles é a seguinte:

//path/to/package:target-name

Se o destino for uma regra, path/to/package será o caminho para o diretório que contém o arquivo BUILD, e target-name será o nome que você nomeou de destino no arquivo BUILD (o atributo name). Se o destino for um destino de arquivo, path/to/package será o caminho para a raiz do pacote e target-name será o nome do arquivo de destino, incluindo o caminho completo.

Ao referenciar destinos na raiz do repositório, o caminho do pacote está vazio, basta usar //:target-name. Ao referenciar destinos no mesmo arquivo BUILD, é possível até ignorar o identificador raiz // do espaço de trabalho e usar apenas :target-name.

Por exemplo, para destinos no arquivo java-tutorial/BUILD, não era necessário especificar um caminho de pacote, já que a raiz do espaço de trabalho é um pacote (//) e seus dois rótulos de destino eram simplesmente //:ProjectRunner e //:greeter.

No entanto, para destinos no arquivo //src/main/java/com/example/cmdline/BUILD, era preciso especificar o caminho completo do pacote de //src/main/java/com/example/cmdline, e seu rótulo de destino era //src/main/java/com/example/cmdline:runner.

Empacotar um destino Java para implantação

Agora, vamos empacotar um destino Java para implantação criando o binário com todas as dependências do ambiente de execução. Isso permite executar o binário fora do ambiente de desenvolvimento.

Como você se lembra, a regra de compilação java_binary produz um .jar e um script de shell de wrapper. Confira o conteúdo de runner.jar usando este comando:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner.jar

O conteúdo é:

META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class

Como você pode notar, runner.jar contém Runner.class, mas não a dependência dele, Greeting.class. O script runner gerado pelo Bazel adiciona greeter.jar ao caminho de classe. Portanto, se você deixá-lo assim, ele será executado localmente, mas não será executado de maneira independente em outra máquina. Felizmente, a regra java_binary permite criar um binário implantável independente. Para criá-lo, anexe _deploy.jar ao nome do destino:

bazel build //src/main/java/com/example/cmdline:runner_deploy.jar

O Bazel produz uma saída semelhante a esta:

INFO: Found 1 target...
Target //src/main/java/com/example/cmdline:runner_deploy.jar up-to-date:
  bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar
INFO: Elapsed time: 1.700s, Critical Path: 0.23s

Você acabou de criar o runner_deploy.jar, que pode ser executado de forma independente fora do seu ambiente de desenvolvimento, já que ele contém as dependências de ambiente de execução necessárias. Confira o conteúdo desse JAR autônomo usando o mesmo comando de antes:

jar tf bazel-bin/src/main/java/com/example/cmdline/runner_deploy.jar

O conteúdo inclui todas as classes necessárias para a execução:

META-INF/
META-INF/MANIFEST.MF
build-data.properties
com/
com/example/
com/example/cmdline/
com/example/cmdline/Runner.class
com/example/Greeting.class

Leia mais

Confira mais detalhes em:

Divirta-se!