Innovation never stopped in Java land. In this article, I am going to explore a new trend of Java Framework: “Build Time Boot” techniques, which make java applications slim and run faster.
If you searched the microservice framework, you will find a lot of options like: Spring Boot, Vertx, Quarkus, Micronaut, Helidon(those are all java), GoMicro(Go), Molecular(nodejs) etc.
Do you know why so many Java framework options?
Java is a 25 years old language, so the landscape and market definitely is much larger than other languages. It shapes the framework landscapes and gives you a lot of choice in the ecosystem.
What is Build Time Boot?
This is an interesting expression. People might think it is insane to say boot in build time if they don’t know the context.
What it really means is that moving tasks usually done in the startup to the build phase.
Yes, actually this has become a popular technique for modern microservice frameworks like quarkus, micronauts.
Traditional Application Server can’t start up fast
In tradition, Application Server was valued by heavy lifting a lot of common work, preparing the beans, classloading, preparing the context for java reflections, preparing thread pools etc. So in that time, it makes sense for an application server to do it once and do it for all.
That’s the main reason application servers get bigger and slower. It’s not a problem but actually a good strategy since it’s serving as a common platform for a long running phase and feeding lots of applications.
Time flies, under modernized application architecture, traditional application servers have been switching their role to serve only one microservice, it makes less and less sense to do a “lot” of pre work before they are ready to serve since you only need to care about one service only.
We want it faster and smaller, there will be no re-deploy, hot-deploy scenario. The big platform scenario disappears and small runtime scenario arise.
Spring boot like java framework “make jar, no wars” address most of these issues. Your application and application server now start at the time and run as one.
But it is still slow if you compare it to its neighbour like nodejs framework or golang framework. Spring Boot style application at least takes about 2+ seconds to start, and about 4 seconds if you count time to first response.
Let’s dig into some details and see what actually causes the problem and where and how the problem can improve.
First let’s setup our little experiment environment，make sure that we have some common and basic tasks for web application such as using java annotation, CDI etc.
You can find out the completed experiment code and log here, it includes both quarkus and spring boot quickstart scenarios.
This experiment is only an attempt to compare how “Build Time Boot” changes the way we do java framework and application server. It ‘s not to prove or argue which framework is the best. I use and like both spring boot and quarkus. I picked spring boot and quarkus as representative as a traditional and modernize framework but you can regard others like wildfly swarm, micronaut etc as the same.
Classloading too many things
There are too many things loaded. Have a peek at how many classes are loaded. Surprisingly the number is so large even for popular lightweight java framework like “Spring Boot”.
Spring Boot load classes: 6022
Quarkus load classes: 3357
There are several reasons for the odds:
- Quarkus used a much more lightweight vertx as servlet core and Spring boot uses a heavier Tomcat as we all knew.
2. The framework itself for providing easy use and productivity is much lightweight，including CDI, Config handling, validation, servlet implementation etc etc;
3. Another recipe here is quarkus is doing a lot of “Built Time Boot” work in the build phase instead of runtime phase. So it eliminates a lot of classes which become unused in runtime.
Too many Metadata processing tasks
There are well known common tasks a framework needs to uplift for developers.
- Parse configuration files like xml, yaml json etc
- Handle java annotations , classpath scan
- Handle CDI
- Java reflection, dynamic preparation, indexing, caching task.
Do they cost a lot of time?
Yes. Considering a real world application depends a lot of dependencies , he preparing, parsing and caching for tasks like annotation, bean creation, method, field indexing etc will snowball to a big size.
Have a look at a very simple spring boot application startup log ( remember to open the “TRACE” level log). It’s not convinient to paste the log here since it’s too large, but you will see tons of metadata processing related work. Bear in mind this is an empty spring boot application. It will get much heavier for more complex application.
On the contrary, you can’t find those tasks are appears in quarkus bootstrap log.
Why? Does quarkus skip all those important tasks?
Not at all, it moves.
How Build Time Boot works
Summary why traditional application server startup slow first:
- Too many class being loaded
- Too much metadata processing time for handling annotation, cdi, reflection, proxy etc;
Now let’s see how we can do differently in “Build Time Boot”.
The philosophy is simple, proceeding everything you could at build time and only leaving the have-to part invocation at runtime. For example, we could process the following in build time instead of runtime.
- Configuration can be parser and turn into byte code at build time.
- Java Annotation can be decomposed into a direct method call at build time
- CDI beans can be built up at build time.
- All the reflection, proxy behaviour can be analysed and turn into executable bytecode at build time.
But how? you might ask.
The answer is “Recording and Replay”
Recorded the boot at the build time, Replay it at runtime
If you check the quarkus build log, you can spot
[INFO] — — quarkus-maven-plugin:1.8.3.Final:build (default) @ quarkus-getting-started — -
[DEBUG] [io.quarkus.deployment.QuarkusAugmentor] Beginning Quarkus augmentation
Starting step …
Finished step …
Starting step …
Finished step …
What it does by those steps are “recording”, the “tapes” the quarkus recording is the runtime invocable bytecode so that the heavy lifting metadata processing, construction, optimization etc won’t be needed anymore in the runtime.
The job is done by quarkus-maven-plugin. It will mock the web application startup process in the build time phase and do all the metadata processing work as much as possible and output them as bytecode.
First let’s have a view of what actually quarkus is built and generated extra during this process, you can review the class list in https://github.com/ryanzhang/buildtimeboot/blob/main/quarkus-build-class.txt.
By compare, here is the spring boot list: https://github.com/ryanzhang/buildtimeboot/blob/main/springboot-build-class.txt
Two things to notice:
- Quarkus generated CDI Bean Class.
You can see _Bean.class and _ClientProxy.class. These classes are usually generated in runtime. But quarkus moves them into build time.
2. Quarkus generated a list of recorded bytecode. You can view it in quarkus-build-class.txt.
Let me show you the invocation part which call the “recorded bytecode” at runtime.
So basically what quarkus do here:
- Recorded many metadata process tasks into bytecodes, so it can be loaded directly without computing them everytime the application starts.
- Put them into a static or main method so they can be invoked directly without processing again. They appeared in the picture right.
note: the recorded bytecode is under io/quarkus/deployment/steps/
the invocatio code path is io/quarkus/runner/ApplicationImpl.class
Also bear in mind the generated class can be much larger in a real world application since it would depend more libaries and framework such as hibernate, jdbc, etc etc.
Because the bytecode manipulating and writing is a very complicated task. So what quarkus does is mocking those “boot” tasks and splitting them into small build steps in the background thread in build time and using “proxy” to record those steps into byte code.
If you want to know more of the details how the recording is done, here is good reference doc.
BTW, you probably would notice that quarkus intentionally doesn’t put lib/ folder into the runnable jar for container image optimization purposes since when you rebuilt the images so the same lib container image layer can be cached.
Gun and Bullet
So “Build Time Boot” in quarkus is implemented by plugins + extensions. A quarkus extension is an optimized jar that can make “recording” possible in the build time.
The maven/gradle plugin is the gun. And the quarkus extension is the bullet.
As long as you picked up the quarkus maven plugin and included the optimized jar. You don’t need to change anything you did for java programming. It’s still the old good taste. That means you can still use CDI, Java annotation, even java reflection if you have to.
(It’s wise to not use java reflection but in some scenarios you can’t change the code already existed.)
So from a developer perspective , the bullet is very important. Otherwise, it will be like holding a best gun without a bullet.
Fortunately, there is a wide variety of popular frameworks and libraries already available for java development. You can find it in https://code.quarkus.io/
You can still use or mix the normal jar if they do not appear in the quarkus community. It should cause no problem but you would not likely have those optimized effects mentioned for the normal jar.
To summary , I think, Build Time Boot is a really great idea for java framework. It does all the heavy work for you and you don’t need to change the way you program. For the Java framework you can still enjoy the great experience of Java ‘s taste, but don’t need to tolerate the inefficient running. You have your cake and eat it.