Why Bazel and Micronaut?
At SUM Global, we build a lot of microservice based software. In doing so, we are also building the web based front ends and mobile applications that access this infrastructure. This sort of build is one of Bazel’s primary use cases. The Polyglot languages, the container creation and the publishing to Kubernetes are all things Bazel does really well, so it’s the obvious choice for the build.
Micronaut is a wonderful framework that creates super tight jvm bytecode that uses annotations to do ahead of time compilation. This creates bytecode that starts up very fast. The framework makes it easy to code the stuff needed for a typical Microservice and has many nice baked in features. In a green field development effort, it’s our goto framework. In addition it works nicely with Kotlin which is our favorite language for most all JVM development including Android, but I digress … Basically we picked the tools because by themselves they are the best tools for the job.
Good apart, not so good together
So, as this seemingly natural fit between our prefered tool stack came together, it was quickly obvious that the Micronaut CLI (which is very helpful) generates Gradle, and there is a lot going on under the covers that is not intuitively obvious. Converting the initial generated Gradle build and converting it to a Build.bazel file had its challenges. First, let me explain a bit. Since Micronaut (MN) wants to do all its work ahead of time as far as the compile goes, it requires some specialized annotation processors that inject dependencies into the build as well as generates a fair amount of code. This is one of the things we love about MN but also adds to the complexity of your
BUILD.bazel file and to the
build.gradle file as well, although nicely hidden in plugin logic.
Adding to the problem set
To complicate this up just a bit more, the Micronaut code is Kotlin. For those familiar with Bazel, you already know this means adding another rule to the mix. Bazel has some nice very nice annotation processor hooks in rules_java and as it turns out also in rules_kotlin. The tricky part was figuring out the "when/where/what" Micronaut needed to have a happy running application. Without any of the annotation processing, the code built with Bazel, but obviously would not run.
How do we solve this?
For this example we are using
rules_external_java for our dependency management,
rules_java for our packaging and
rules_kotlin for handling our Kotlin specific things. Here are the versions of things used:
- Bazel version is 2.2.0: https://github.com/bazelbuild/bazel/releases
- Kotlin version is 1.3.70: https://github.com/JetBrains/kotlin/releases/tag/v1.3.70
- Micronaut version is 1.3.2: https://micronaut.io/download.html
- Java 11 LTS, specifically I am using the Azul OpenJDK 11 latest.
First thing we need to do is set up our Kotlin toolchain to be sure we are getting what we expect. To do this your define the tool chain in your BUILD.bazel file like this:
KOTLIN_LANGUAGE_LEVEL="1.3" # "1.1", "1.2", or "1.3" JAVA_LANGUAGE_LEVEL="11" # "1.6", "1.8", "9", "10", "11", or "12" define_kt_toolchain( name = "kotlin_toolchain", # register in the workspace api_version = KOTLIN_LANGUAGE_LEVEL, jvm_target = JAVA_LANGUAGE_LEVEL, language_version = KOTLIN_LANGUAGE_LEVEL, )
And its registered in the WORKSPACE file like this:
The more interesting piece is defining the annotation processor for the code so Micronaut can do its ahead of time compilation. In Bazel, you need to define a java_plugin. This consists of the code to execute, and the runtime dependencies of that code. This was the hidden gem that made these two wonderful tools come together. Here is how you define one of the
java_plugin(s) for Micronaut:
java_plugin( name = "micronaut_inject_plugin", 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", ], )
There is a second one needed to complete most of what Micronaut is doing and I got very imaginative with the names here as you will see:
java_plugin( name = "micronaut_inject_plugin2", 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", ], )
And finally to tie this all together and make sure we pick up the output of the two plugins we create a little java_library definition that we can put in our Kotlin dependencies by name. Keeping things clean.
java_library( name = "micronaut_lib", exported_plugins = ["micronaut_inject_plugin","micronaut_inject_plugin2"], exports = [ "@maven//:io_micronaut_micronaut_inject_java", "@maven//:io_micronaut_micronaut_aop", "@maven//:io_micronaut_micronaut_inject", "@maven//:io_micronaut_micronaut_validation", ], )
We make sure to export the processor dependencies because the generated code has dependencies on it. Now I will be honest here, I didn’t take the time to slim this down to its bare minimum, so there may be something in the export list that is not strictly required for this example, and I heavily borrowed from a much larger build with a much more complex usage of Micronaut. Please forgive any unnecessary bloat.
To wrap this up, we have a section (you can see it in the full Github project, link is below) with your standard dependencies for the application code, and finally tying it all together, we create a kt_jvm_library that compiles our Kotlin and the java_binary.
kt_jvm_library( name = "app_lib", srcs = glob(["src/main/kotlin/**/*.kt"]), deps = [ ":micronaut_lib", ":java_deps", ], ) java_binary( name = "bazel-micronaut-example", main_class = "com.sumglobal.Application", runtime_deps = [":app_lib"], )
Summing it all up
First, if you pulled the code (link below) from Github, and have compiled it with:
bazel build //:bazel-micronaut-example_deploy.jar to create a nice FAT jar, then you can crank up the service with the command line:
java -jar bazel-bin/bazel-micronaut-example_deploy.jar. This puts out a nice little log message telling you things are up and happily running, so our annotation processors worked. Then you can make sure thing actually work by doing a simple curl command to http://localhost:9980/bazel-mn/hello or http://localhost:9980/bazel-mn/goodbye. You can also just click the links.
What the production project got from this, was a single build tool for the front end web application, the Android app and the backend services. The build for a single service is approximately 3x faster (after the first run) than the Gradle build, and in the end, we had a lot more control over what the build is doing. It’s not the easiest path to walk down with Micronaut, but once you get the trail blazed, it’s a much better overall experience. Give it a try and let us know what you think.
You can find the full code in our Github repo here: https://github.com/SUMGlobal/bazel-micronaut-example