How to Build Java Projects with Bazel

9 minute read     Updated:

Mdu Sibisi %
Mdu Sibisi

We’re Earthly. We make building software simpler and therefore faster using containerization. This article is about building Java projects with Bazel. If you want to see what can be done by combining ideas from a Makefile and a Dockerfile then check us out.

If you’re a Java developer, you’ll have likely used ANT, Maven, or Gradle to build your projects. Many of these solutions have been around for decades, but Java’s changed a lot over the years. While these tools are still serviceable, you may want something more conducive to the current speed of application development and deployment.

Bazel is an open-source automatic build tool that has more to offer in the way of extensibility, scalability, and flexibility. It was developed by Google, but it’s suitable for organizations of any size that want to accelerate builds and reduce build configuration time. Because Bazel uses cached artifacts and rebuilds only what’s changed in the code, it can build complex code bases quickly.

While Bazel supports a large host of programming languages and platforms, let’s focus on how to use it specifically with Java. This tutorial walks you through a simple project showcasing Bazel’s key capabilities.

Prerequisites

I’m assuming a couple of things before you move on:

You can find the project files for this tutorial on GitHub. Note that this tutorial uses the Windows 10 OS, but Bazel runs on macOS and Linux as well.

Installing Bazel

Currently, the best way to install Bazel is using Bazelisk. This requires you to install and configure Chocolatey. Once that’s done, open a terminal window or emulator (Command Prompt, PowerShell, etc.) with administrator permissions.

Run the following command:

choco install bazelisk

When asked if you want to install the script, enter A for ALL.

Create Your Java Project File

Create a folder called BazelExample to house your project, then create a simple Java class. Note that your class must use an external dependency. As an example, the following sample code uses the TextIO library to create a simple console application that prompts users to input their usernames and passwords:

package com.bazel.example;
import org.beryx.textio.TextIO;
import org.beryx.textio.TextIoFactory;


public class Salutations {
    
    public static void main() {
        TextIO textIO = TextIoFactory.getTextIO();

        String user = textIO.newStringInputReader()
            .withDefaultValue("admin")
            .read("Username");

        String password = textIO.newStringInputReader()
            .withMinLength(6)
            .withInputMasking(true)
            .read("Password");

    }
}

I recommend that you use the Maven standard directory layout, like so:

BazelExample
├── src
│   └── main
│       └── java
│           └── com
│               └── bazel
│                  └─ example
│                     └── Salutations.java                   

Configure Your Workspace

To use Bazel, you must assign a workspace to contain your source files, resources, and output builds. Navigate to the root of your project folder (eg, BazelExample) and create an extension-less file called WORKSPACE.

The next step is to create a build file for the project.

Create a Build File

Build files tell Bazel where to find your source files and how to build them. Again, you need to create an extension-less file at the root of your project’s directory. This time, call it BUILD. Its contents should be as follows (or similar):

load("@rules_java//java:defs.bzl", "java_binary")

java_binary(
    name = "Salutations",
    srcs = ["src/main/java/com/bazel/example/Salutations.java"],
    main_class = "com.bazel.example.Salutations",
)

In addition to identifying sources, BUILD contains a set of instructions and configurations known as rules. The first thing the BUILD file tells Bazel to do is load the java_binary rule from the rules_java extension (load("@rules_java//java:defs.bzl", "java_binary")). It tells Bazel to package the compiled files into a .jar file and creates a wrapper shell script called a target.

The java_rule contains a set of arguments for configuring the final binary (.jar) file. name dictates what the final binary is named, srcs specifies the source files to compile and build, and main _class indicates which class should run the program.

You should only have one source file at this moment, but it’s possible to specify multiple files. Use the glob function to add wildcards and include or exclude multiple files without listing them manually:

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

You now have all the essentials required to build this tutorial’s project. However, the sample code has external dependencies, so keep in mind the build and application won’t work without them.

Adding Dependencies and Splitting Your Project

Adding

So you’ve established the foundation of your workspace and build. Now you need to add TextIO as an external dependency, or you’ll run into a compilation error.

Adding an External Dependency

To add a mechanism to fetch external dependencies and add them to your build, open the WORKSPACE file you created earlier in a source code editor of your choice. Add the following contents to it:


load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

#USE Latest version of RULES_JVM_EXTERNAL
RULES_JVM_EXTERNAL_TAG = "5.1"
RULES_JVM_EXTERNAL_SHA ="8c3b207722e5f97f1c83311582a6c11df99226e65e2471086e296561e57cc954"

#Fetches Extension
http_archive(
    name = "rules_jvm_external",
    strip_prefix = "rules_jvm_external-%s" % RULES_JVM_EXTERNAL_TAG,
    sha256 = RULES_JVM_EXTERNAL_SHA,
    url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/%s/rules_jvm_external-%s.tar.gz" % (RULES_JVM_EXTERNAL_TAG, RULES_JVM_EXTERNAL_TAG)
)

#rules_jvm_external depends on this library. 
http_archive(
    name = "bazel_skylib",
    sha256 = "b8a1527901774180afc798aeb28c4634bdccf19c4d98e7bdd1ce79d1fe9aaad7",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz",
        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.1/bazel-skylib-1.4.1.tar.gz",
    ],
)

The first thing the code does is load an HTTP Repository Rule (http_archive). Again, Bazel needs some way to fetch the necessary dependency files, hence the addition of the rules_jvm_external extension. It enables Bazel to retrieve the right libraries from the right sources.

I recommend that you use the latest version of the extension. At the time of writing, that’s version 5.1.

You may notice that the code fetches the bazel_skylib library, too. The latest versions of the rule_jvm_external extension depend on it, so the build would fail without its inclusion.

Now that you’ve added the rules_jvm_external extension, you have to tell it what to do. Add the following code to your WORKSPACE file:

load("@rules_jvm_external//:defs.bzl", "maven_install")

maven_install(
    artifacts = [
        "org.beryx:text-io:3.4.1",
        
    ],
   
    repositories = [
        "https://maven.google.com",
        "https://repo1.maven.org/maven2",
        "https://jcenter.bintray.com/",
        "http://uk.maven.org/maven2",
    ],
)

This uses the maven_install rule from the rules_jvm_external extension to specify which dependencies to fetch and which repositories to look in. The first argument, artifacts, accepts a list of artifact names. The second, repositories, contains a list of repositories. The more repos you add, the more likely it is that Bazel will find the dependency.

Once you’re finished editing the WORKSPACE file, save and close it.

Now it’s time to edit the BUILD file. Edit the java_binary rule’s argument list:

java_binary (
    name = "Salutations",
    main_class = "com.bazel.example.Salutations",
    srcs = ["src/main/java/com/bazel/example/Salutations.java"],  
    deps = ["@maven//:org_beryx_text_io"], 
)

This adds the org.beryx.text-io dependency to your project. The deps rule uses the Canonical path to point toward the dependencies. Any slashes and dashes in the package name are replaced with underscores (_).

That’s how you add external dependencies, but what about local dependencies? The best way to add a local dependency is to split your project and create separate targets for your local libraries or class files.

Splitting Your Project and Adding Local Dependencies

Create a new folder in the src/main/java/com/bazel/example/ folder called greetings. Next, create a new Java file called Greeter.java and add the following:

package com.bazel.example.greetings;

public class Greeter {
    public static void sayHello() {
        System.out.println("Hello");
    }

}

Import the Greeter class and add a call to the sayHello() method from the main method of Salutations.java. The code should now look similar to this:

package com.bazel.example;

import com.bazel.example.greetings.Greeter;
import org.beryx.textio.TextIO;
import org.beryx.textio.TextIoFactory;


public class Salutations {
    
    public static void main(String [] args) {
        Greeter.sayHello();
        TextIO textIO = TextIoFactory.getTextIO();

        String user = textIO.newStringInputReader()
        .withDefaultValue("admin")
        .read("Username");

        String password = textIO.newStringInputReader()
        .withMinLength(6)
        .withInputMasking(true)
        .read("Password");

    }
}

To add it to the BUILD file, first add the java_library to the load function’s list of arguments:

load("@rules_java//java:defs.bzl", "java_binary", "java_library")

Add the target:

java_library(
    name = "Greeter",
    srcs = ["src/main/java/com/bazel/example/greetings/Greeter.java"],
)

And finally, add the new target as a dependency:

java_binary (
    name = "Salutations",
    main_class = "com.bazel.example.Salutations",
    srcs = ["src/main/java/com/bazel/example/Salutations.java"],  
    deps = ["@maven//:org_beryx_text_io",":Greeter"], 
)

Your BUILD file should now look like this:

load("@rules_java//java:defs.bzl", "java_binary", "java_library")

java_library(
    name = "Greeter",
    srcs = ["src/main/java/com/bazel/example/greetings/Greeter.java"],
)

java_binary (
    name = "Salutations",
    main_class = "com.bazel.example.Salutations",
    srcs = ["src/main/java/com/bazel/example/Salutations.java"],  
    deps = ["@maven//:org_beryx_text_io",":Greeter"], 
)

Build Your Project

Now let’s go back to the terminal and build our project. Navigate to the root folder of your project’s directory structure and run the following command:

bazel build //:Salutations

This builds your project and combines all the targets into a single package. The initial build may take a while, as Bazel has to download the necessary files in addition to compiling your class files. However long it takes, the final output should look similar to this:

Bazel Build Command

Use your file explorer to navigate to the project folder, and you’ll find a collection of new folders and shortcuts. You can run the application by using:

bazel-bin/Salutations        

⚠You may encounter a warning message in regard to a missing SLF4J logger. You can ignore this.

To test your Java application, follow the prompts to enter a username and password.

Run the application

Generating a Dependency Graph with Bazel

It can be a bit difficult to keep track of all the dependencies your project relies on. Sure, you can generate a directory structure and use that as a guide. However, Bazel allows you to generate dependency graphs to help you visualize the connections between your projects.

You can build these graphs using the Bazel query command:

bazel query "deps(//:Salutations)" --output graph

That should return a long wall of text. Copy the output and paste it into Graphviz. You’ll notice that the graph is extremely noisy. That’s because the data features all dependencies, including tooling and transitive and implicit dependencies that Bazel downloads and adds to your project.

You can simplify the graph by querying only the dependencies that are relevant to you and the core of your project:

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

The above query ignores tooling and implicit dependencies, so the output should now be far more manageable:

digraph mygraph {
  node [shape=box];
  "//:Salutations"
  "//:Salutations" -> "//:salutations-lib"
  "//:salutations-lib"
  "//:salutations-lib" -> "//:src/main/java/com/bazel/example/Salutations.java"
  "//:salutations-lib" -> "@maven//:org_beryx_text_io"
  "//:src/main/java/com/bazel/example/Salutations.java"
  "@maven//:org_beryx_text_io"
  "@maven//:org_beryx_text_io" -> "@maven//:v1/https/repo1.maven.org/maven2/org/beryx/text-io/3.4.1/text-io-3.4.1-sources.jar\n@maven//:v1/https/repo1.maven.org/maven2/org/beryx/text-io/3.4.1/text-io-3.4.1.jar"
  "@maven//:org_beryx_text_io" -> "@maven//:jline_jline"
  "@maven//:org_beryx_text_io" -> "@maven//:org_slf4j_slf4j_api"
  "@maven//:org_beryx_text_io" -> "@maven//:org_beryx_awt_color_factory"
  "@maven//:org_beryx_awt_color_factory"
  "@maven//:org_beryx_awt_color_factory" -> "@maven//:v1/https/repo1.maven.org/maven2/org/beryx/awt-color-factory/1.0.1/awt-color-factory-1.0.1-sources.jar\n@maven//:v1/https/repo1.maven.org/maven2/org/beryx/awt-color-factory/1.0.1/awt-color-factory-1.0.1.jar"
  "@maven//:v1/https/repo1.maven.org/maven2/org/beryx/awt-color-factory/1.0.1/awt-color-factory-1.0.1-sources.jar\n@maven//:v1/https/repo1.maven.org/maven2/org/beryx/awt-color-factory/1.0.1/awt-color-factory-1.0.1.jar"
  "@maven//:org_slf4j_slf4j_api"
  "@maven//:org_slf4j_slf4j_api" -> "@maven//:v1/https/repo1.maven.org/maven2/org/slf4j/slf4j-api/1.8.0-beta4/slf4j-api-1.8.0-beta4.jar\n@maven//:v1/https/repo1.maven.org/maven2/org/slf4j/slf4j-api/1.8.0-beta4/slf4j-api-1.8.0-beta4-sources.jar"
  "@maven//:v1/https/repo1.maven.org/maven2/org/slf4j/slf4j-api/1.8.0-beta4/slf4j-api-1.8.0-beta4.jar\n@maven//:v1/https/repo1.maven.org/maven2/org/slf4j/slf4j-api/1.8.0-beta4/slf4j-api-1.8.0-beta4-sources.jar"
  "@maven//:jline_jline"
  "@maven//:jline_jline" -> "@maven//:v1/https/repo1.maven.org/maven2/jline/jline/2.14.6/jline-2.14.6-sources.jar\n@maven//:v1/https/repo1.maven.org/maven2/jline/jline/2.14.6/jline-2.14.6.jar"
  "@maven//:v1/https/repo1.maven.org/maven2/jline/jline/2.14.6/jline-2.14.6-sources.jar\n@maven//:v1/https/repo1.maven.org/maven2/jline/jline/2.14.6/jline-2.14.6.jar"
  "@maven//:v1/https/repo1.maven.org/maven2/org/beryx/text-io/3.4.1/text-io-3.4.1-sources.jar\n@maven//:v1/https/repo1.maven.org/maven2/org/beryx/text-io/3.4.1/text-io-3.4.1.jar"
}

Copy and paste the output once again into the Graphviz generator, and the hierarchy should look something like this:

Graphviz Bazel graph

Package the Binary for Deployment

At this point, you can’t really run salutations.jar, at least not on its own. That’s because it doesn’t contain the dependencies. You can check that by running the following command from your projects root:

jar -tf bazel-bin/Salutations.jar

It should reveal the following metadata:


META-INF/
META-INF/MANIFEST.MF
com/
com/bazel/
com/bazel/example/
com/bazel/example/Salutations.class

So you need to build a special deployment .jar:

bazel build //:Salutations_deploy.jar

There’s now a new file in your bazel-bin folder called Salutations_deploy.jar. Look at what’s inside the .jar by running the following command:

jar -tf bazel-bin/Salutations_deploy.jar

This should return a list of all the classes packed into the .jar. The list is too long to share here, but you can either run this .jar on its own or you can use the executable, depending on the project’s needs.

Conclusion

We’ve walked through the basics of building Java projects with Bazel in this tutorial. Now you should have a rough idea of how Bazel can improve your workflows and big project builds. If you enjoyed learning about Bazel, then you’ll love exploring Earthly - another nifty build automation tool for your tech stack. Check it out!

Happy coding!

Earthly makes CI/CD super simple
Fast, repeatable CI/CD with an instantly familiar syntax – like Dockerfile and Makefile had a baby.

Learn More

Mdu Sibisi %
Mdu Sibisi

I’ve worked for over 10 years as a software developer and I primarily specialize in object-orientated languages. As a blogger, I’ve been writing for eight years.

Writers at Earthly work closely with our talented editors to help them create high quality tutorials. This article was edited by:
Bala Priya C

Bala is a technical writer who enjoys creating long-form content. Her areas of interest include math and programming. She shares her learning with the developer community by authoring tutorials, how-to guides, and more.

Published:

Get notified about new articles!
We won't send you spam. Unsubscribe at any time.