Build Micronaut Applications with Bazel

In this post, I will show you how to build Micronaut applications with Bazel. This is the first step in creating a mono repository for a complete web application (Backend and Frontend) that leverages incremental builds with Bazel. This tutorial is intended for Mac users, but It should be straightforward to get it working on other platforms.

Bazel

Bazel is a versatile build tool developed at Google used to speed up the build process of their mono-repository. As described here, Google uses a mono-repository and Bazel to host and build billions of lines of code. One key aspect about using Bazel is that it allows you to leverage mono-repository without compromising the build performance by leveraging incremental builds (It avoids building modules that have not changed).

Pre-requisites

Before you dive in, make sure you have the following dependencies installed. You can use the links provided below.

Build the project

In this section, I will show a step by step how to create and build a Micronaut project using Bazel.

First of all, we need to create the project structure to host our code.

mkdir bazel-micronaut && cd bazel-micronaut
mkdir src
touch WORKSPACE.bazel
touch BUILD

The WORKSPACE file lives at the root of the project and is used to define the BAZEL workspace.

The BUILD file is generally used to define how a package is built.

The next step is to create the Micronaut project.

cd src
mn create-app --build=maven --jdk=8 --lang=java --test=junit com.example.backend

Now that we have all the resources created, we are going to configure Bazel to build our application. The first step to do is to configure Bazel workspace and define the list of maven dependencies we need for this project.

Open the file WORKSPACE.bazel and paste the following:

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

RULES_JVM_EXTERNAL_TAG = "4.0"
RULES_JVM_EXTERNAL_SHA = "31701ad93dbfe544d597dbe62c9a1fdd76d81d8a9150c2bf1ecf928ecdf97169"
MICRONAUT_VERSION = "2.5.7"
MICRONAUT_TEST_VERSION = "2.3.7"
JUNIT_VERSION = "5.7.1"
LOGBACK_VERSION = "1.2.3"

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/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

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

maven_install(
    artifacts = [
        "io.micronaut:micronaut-aop:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-inject:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-inject-java:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-validation:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-http-client:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-http-server-netty:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-runtime:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-context:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-core:%s" % MICRONAUT_VERSION,
        "ch.qos.logback:logback-core:%s" % LOGBACK_VERSION,
        "ch.qos.logback:logback-classic:%s" % LOGBACK_VERSION,
        "javax.annotation:javax.annotation-api:1.3.2",
        "org.slf4j:slf4j-api:1.7.30",
        "io.micronaut.test:micronaut-test-junit5:%s" % MICRONAUT_TEST_VERSION,
        "org.junit.jupiter:junit-jupiter-api:%s" % JUNIT_VERSION,
        "org.junit.jupiter:junit-jupiter-engine:%s" % JUNIT_VERSION
    ],
    repositories = [
        "https://repo.maven.apache.org/maven2"
    ],
)

After defining the dependencies, and in order to improve build time, and artifacts resolution, it’s advised to in maven dependencies. This step also enables you to run builds in offline mode.

bazel run @maven//:pin

After pinning maven dependencies, we need to update the file WORKSPACE.bazel to instruct Bazel to use pinned dependencies. The updated version of this file should look like the following:

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

RULES_JVM_EXTERNAL_TAG = "4.0"
RULES_JVM_EXTERNAL_SHA = "31701ad93dbfe544d597dbe62c9a1fdd76d81d8a9150c2bf1ecf928ecdf97169"
MICRONAUT_VERSION = "2.5.7"
MICRONAUT_TEST_VERSION = "2.3.7"
JUNIT_VERSION = "5.7.1"
LOGBACK_VERSION = "1.2.3"

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/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

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

maven_install(
    artifacts = [
        "io.micronaut:micronaut-aop:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-inject:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-inject-java:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-validation:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-http-client:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-http-server-netty:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-runtime:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-context:%s" % MICRONAUT_VERSION,
        "io.micronaut:micronaut-core:%s" % MICRONAUT_VERSION,
        "ch.qos.logback:logback-core:%s" % LOGBACK_VERSION,
        "ch.qos.logback:logback-classic:%s" % LOGBACK_VERSION,
        "javax.annotation:javax.annotation-api:1.3.2",
        "org.slf4j:slf4j-api:1.7.30",
        "io.micronaut.test:micronaut-test-junit5:%s" % MICRONAUT_TEST_VERSION,
        "org.junit.jupiter:junit-jupiter-api:%s" % JUNIT_VERSION,
        "org.junit.jupiter:junit-jupiter-engine:%s" % JUNIT_VERSION
    ],
    repositories = [
        "https://repo.maven.apache.org/maven2"
    ],
    maven_install_json = "//:maven_install.json"
)

load("@maven//:defs.bzl", "pinned_maven_install")
pinned_maven_install()

To pin new dependencies, you can the following command:

bazel run @unpinned_maven//:pin

The next step is to configure the build file to instruct Bazel how to build our application. It’s important to mention that Micronaut uses annotation processor, and we need to configure them appropriately. The content of the file BUILD should look like the following:

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

java_plugin(
    name = "type_element_annotation_processor",
    processor_class = "io.micronaut.annotation.processing.TypeElementVisitorProcessor",
    deps = [
        "@maven//:io_micronaut_micronaut_inject_java",
        "@maven//:io_micronaut_micronaut_aop",
        "@maven//:io_micronaut_micronaut_inject",
        "@maven//:io_micronaut_micronaut_validation"
    ]
)

java_plugin(
    name = "aggregating_type_element_annotation_processor",
    processor_class = "io.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessor",
    deps = [
        "@maven//:io_micronaut_micronaut_inject_java",
        "@maven//:io_micronaut_micronaut_aop",
        "@maven//:io_micronaut_micronaut_inject",
        "@maven//:io_micronaut_micronaut_validation"
    ]
)

java_plugin(
    name = "bean_definition_annotation_processor",
    processor_class = "io.micronaut.annotation.processing.BeanDefinitionInjectProcessor",
    deps = [
        "@maven//:io_micronaut_micronaut_inject_java",
        "@maven//:io_micronaut_micronaut_aop",
        "@maven//:io_micronaut_micronaut_inject",
        "@maven//:io_micronaut_micronaut_validation"
    ]
)

java_plugin(
    name = "service_descriptor_annotation_processor",
    processor_class = "io.micronaut.annotation.processing.ServiceDescriptionProcessor",
    deps = [
        "@maven//:io_micronaut_micronaut_inject_java",
        "@maven//:io_micronaut_micronaut_aop",
        "@maven//:io_micronaut_micronaut_inject",
        "@maven//:io_micronaut_micronaut_validation"
    ]
)

java_library(
    name = "micronaut_libs",
    exported_plugins = ["service_descriptor_annotation_processor","type_element_annotation_processor","aggregating_type_element_annotation_processor", "bean_definition_annotation_processor"],
    exports = [
        "@maven//:io_micronaut_micronaut_inject_java",
        "@maven//:io_micronaut_micronaut_aop",
        "@maven//:io_micronaut_micronaut_inject",
        "@maven//:io_micronaut_micronaut_validation"
    ]
)

java_library(
    name = "backend_deps",
    resources = glob(["src/backend/src/main/resources/**/*.xml", "src/backend/src/main/resources/**/*.yml"]),
    exports = [
         "@maven//:ch_qos_logback_logback_classic",
         "@maven//:io_micronaut_micronaut_runtime",
         "@maven//:io_micronaut_micronaut_http_server_netty",
         "@maven//:io_micronaut_micronaut_http_client",
         "@maven//:io_micronaut_micronaut_core",
         "@maven//:io_micronaut_micronaut_context",
         "@maven//:io_micronaut_micronaut_validation",
         "@maven//:io_micronaut_micronaut_aop",
         "@maven//:io_micronaut_micronaut_inject",
         "@maven//:io_micronaut_micronaut_inject_java"
    ]
)

java_library(
    name = "backend_libs",
    srcs = glob(["src/backend/src/main/java/**/*.java"]),
    deps = [
        ":micronaut_libs",
        ":backend_deps",
    ],
)

java_binary(
    name = "backend-services",
    main_class = "com.example.Application",
    runtime_deps = [":backend_libs"]
)

After we have configured all the required files, now we can build the project by running the following command:

bazel build //:backend-services

To run the application, you can run the following command:

 bazel run //:backend-services

If everything works as expected, you should see an output like this:

Summary

In this tutorial, we learnt how to configure Bazel to build and run Micronaut applications. The most important part of this tutorial is configuring the annotation processors that Micronaut relies on to assemble our application at compile time. In the next tutorial, we will look at how to configure Bazel to run tests.

You can find all the source code of this tutorial here.