When you start a new project in Java, obviously you are not going to use only plain vanilla Java. You usually need some libraries or frameworks to help you: maybe to connect to a database, maybe to setup some web service.
There are many of these libraries and frameworks that have been around our Java ecosystem for a long time. They have helped evolved the Java ecosystem for decades. These libraries experiment with features and how to implement them in the best way. That generated a knowledge base that the developers of the language can later use to add those same features in the best way possible to the core language.
De facto standard becoming the new base
And this is something that happens in other languages. For example, if you were a web developer a couple of decades ago, you will remember JQuery. It was the default library that (almost) everybody used to build webpages. JQuery became so popular, that the JavaScript developers decided, in the end, that it made sense to implement the same features on vanilla JavaScript. Because everybody was using it anyway. It was a de facto standard. So, why not made it part of the plain base language?
After JQuery, there came many other libraries for web development. And sadly, although they all implement very similar features, those libraries have diverged a lot. Most of them are based on similar concepts (having a shadow doom, having components,…). But the vocabulary, the grammar, the semantics of how each framework operates are completely different.
The fragmentation of JavaScript
When you take a look at job offers in the web development space, you will no longer see a “Looking for a JavaScript Developer”. Instead, you can see “Looking for a React Developer”, or “Looking for an Angular Developer”. And changing from one framework to another is costly. You would have to trash most of your source code and start from scratch all over again.
That, in my opinion, is problematic in several ways. First, there is no longer a standard that everybody agrees on that can be easily incorporated to the vanilla version of JavaScript. But also, the ecosystem is almost forking in a way that it will be very difficult to join again in the future to build a common ground to reuse knowledge and development efforts. It is not clear in which direction the language should evolve, because each framework is tensing the evolution in a different direction.
Is Java becoming fragmented?
Are we having the same fragmentation in Java? 20 years ago, Spring was kind of our de facto standard library to do everything. It set up some expectations, grammar, vocabulary, that we all shared and understood. But during the past few years there has been new frameworks appearing that did things in a different way, with different goals.
Wouldn’t it be possible to have some common ground? Some common vocabulary, grammar, semantics, understandings, that we all shared? Leaving implementation details to each particular framework, but can’t we agree on offering similar conceptual features? And then, maybe, let the base language choose the best implementation of all, if any.
For example, if I say “I need a Singleton”, that Singleton has the same behaviour we all understand. I don’t care about implementation details, I just want that instance to behave as theoretically described. If we all use the same way of defining singletons, moving to a different framework should be easier.
Or if I need to connect to a database, we can use a common grammar to define tables, relationships, and columns. Regardless of which library I use to do the actual connection. I just want to make sure it behaves how I want it to behave. If I say a column is an identifier, it should behave as such. If I say there is a many to many relationship, I want that relationship to be there. The concepts are the same, no matter the framework we use.
So, if in the future, the framework I am using is no longer the best for my use case, I want to be able to easily move and adapt, without having to throw all my code away.
MicroProfile
That is the concept of MicroProfile. It is a community-driven vendor-neutral effort that defines concepts, grammar, vocabulary, semantics,… that are abstract enough to be shared across different frameworks and libraries.
“We aim to merge innovation and standardization into Enterprise Java with Microservices focus.”
MicroProfile
Some of you readers may realize this is not something new. This is something we have already done. In 1997, one of the biggest features of Java 6 was the Enterprise Java Beans (EJB). Each framework implemented its own way of handling beans, EJB defined the business logic of beans in Java. Or maybe some readers remember JavaEE (lately renamed to JakartaEE) that also offered some common APIs.
Fear not, MicroProfile is not trying to reinvent the wheel. In fact, MicroProfile has JackartaEE as part of its core, among other API specifications like OpenAPI or Telemetry.
I am not a MicroProfile developer, nor am I part of any of the frameworks I am going to discuss on this article. But after reading about MicroProfile, I wanted to experiment and see for myself how true is that MicroProfile simplifies developing and allows a common playground for all frameworks.
The three MicroProfile misfits
For this exploration, I tried to write in “MicroProfile” for three different frameworks. The idea was to use the same source code to do something very basic: a web server with some database connection. And then see if the three of them can use that same source code.
The first framework would be, of course, Spring. It has been the standard for 20 years already and has strong foundations, documentation, examples, use cases. Lots of experts have grown with Spring. But, of course, Spring has been here long before MicroProfile even existed. It’s not that Spring does lack features. It’s that sometimes expressing those features in MicroProfile is not straight forward. So compatibility is sometimes hard, as some of the semantics and concepts do not fit that well.
The second framework is Micronaut. Micronaut has a strong relationship with GraalVM and Oracle, the biggest contributor of Java. It is focused on Cloud Native and MicroServices. Micronaut follows very closely the MicroProfile specification, but sometimes when it needs a feature not yet available, it creates its own annotations and grammar.
The third framework I tested was Quarkus. I already had some experience with it. But I was most curious because Quarkus heavily rely on MicroProfile. Instead of diverging from it, if they need something that is not on MicroProfile yet, the Quarkus team fights to get some definitions in MicroProfile.
GraalVM
There is one thing that these three frameworks have in common: they all target GraalVM as a runtime of choice. GraalVM improves the Ahead of Time (AoT) compilation of Java programs. It also allows to build native binaries that will run independently of any JVM.
This compilation is based on the Closed World assumption. This means that you have all the information needed on build time. For example, there will be no libraries added dynamically to the classpath and no reflection at runtime. So, if some class or method is not called explicitly by another class or method in your source code, we can safely remove it from the build. This reduces the size of the compiled application, which allows not only for reduced memory usage, but also a faster startup and execution.
The downside is that not all applications are suitable for this kind of compilation. But don’t worry, that’s what Leyden is for.
Do they really speak MicroProfile?
I tried to build a simple web service that connects to an H2 database. As I want to use the same source code for all three frameworks, I created a single Maven project for all of them and use profiles to change the dependencies.
The source code is available here: https://github.com/Delawen/chaos-incarnated.
Disclaimer: I approached the three frameworks as a regular civilian with no previous experience with MicroProfile on them. There may be a simpler or better way of doing things. But, if there is, it is not easy to find in the documentation.
Who is your Daddy?
My first problem down the road is inherently tied to having three frameworks on the same Maven project. But it is also relevant if you have a big hierarchy of projects, with your own parents. Can I use these frameworks with dependency management blocks or do I need to set up a specific parent for them?
Both Quarkus and Spring allow to add a dependency management block that will handle versioning and dependencies. Spring strongly suggests to use the parent in the POM, but it can be used (with some versions missing) using the proper dependency management. Sadly, Micronaut forces us to use a parent in our POM, making the whole profile to change the framework problematic.
As a summary of this section, both Quarkus and Spring pass the test, while Micronaut make it a bit more difficult.
Quarkus | Spring | Micronaut |
😊 | 🤠 | 😵 |
Invoke MicroProfile code
Now, let’s try to create a service we can invoke. The source code for a hello world service in MicroProfile is pretty straight forward:
package com.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class HelloService {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello from MicroProfile!";
}
}
When I tried to run this piece of code, I realized that for Spring and Micronaut, adding the proper dependencies and build sections was not enough. Both of them needed an Application class that bootstrap the webservice [1] [2]. That application class was just decorative, it was not adding anything to our source code, but without it, the service just didn’t run.
The Application class for Spring Boot:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The Application class for Micronaut:
package com.example;
import io.micronaut.runtime.Micronaut;
public class MicronautApplication {
public static void main(String[] args) {
Micronaut.run(MicronautApplication.class, args);
}
}
Also, the Spring and the Quarkus section on the POM is pretty clean: you have a build plugin, some basic dependencies, and it just runs. But the Micronaut section requires several advanced configurations, like Annotation Process Paths. Once you have them configured, you can forget about those. But it is obvious that MicroProfile is not the main focus on development for Micronaut if this is not configured by default.
As a summary of this section, Quarkus is very easy to start with, while Spring and Micronaut made me add extra source code specific for the framework.
Quarkus | Spring | Micronaut |
😊 | 😵 | 😵 |
Let’s connect to a database
The next step was to add some basic database entity I could use. The database definitions in MicroProfile are pretty self explanatory.
With @Entity we define a table with columns:
@Entity
public class Cat implements Serializable {
@NotNull
@Column(name = "name", nullable = false)
private String name;
@Id
@GeneratedValue
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Then I need some kind of management class to run the queries. For simplicity, I made that class a @Singleton:
@Singleton
public class DatabaseResource {
@PersistenceContext
EntityManager entityManager;
@Transactional
public void addCat(String name) {
Cat cat = new Cat();
cat.setName(name);
entityManager.persist(cat);
}
@Transactional
public List<Cat> getAllCats() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Cat> cq = cb.createQuery(Cat.class);
Root<Cat> rootEntry = cq.from(Cat.class);
CriteriaQuery<Cat> all = cq.select(rootEntry);
TypedQuery<Cat> allQuery = entityManager.createQuery(all);
return allQuery.getResultList();
}
And, of course, we need a web service to handle the cats:
@Path("/db")
public class PersistenceService {
@Inject
DatabaseResource databaseResource;
@GET //For testing purposes
@Path("/cat/{name}")
@Produces(MediaType.TEXT_PLAIN)
@Transactional
public String addCat(@PathParam("name") String name) {
databaseResource.addCat(name);
return "Cat added with name " + name + " via GET.";
}
@POST
@Path("/cat/{name}")
@Produces(MediaType.TEXT_PLAIN)
@Transactional
public String addCatPost(@PathParam("name") String name) {
databaseResource.addCat(name);
return "Cat added with name " + name;
}
@GET
@Path("/cats")
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public List<Cat> getCats() {
return databaseResource.getAllCats();
}
}
All the database related source code is still part of Jakarta APIs, which are MicroProfile.
Micronaut needs a specific annotation that can be placed on the MicronautApplication class, to know it has to process the @Entity class:
package com.example;
import com.example.model.Cat;
import io.micronaut.runtime.Micronaut;
import io.micronaut.serde.annotation.SerdeImport;
@SerdeImport(Cat.class)
public class MicronautApplication {
public static void main(String[] args) {
Micronaut.run(MicronautApplication.class, args);
}
}
On this section, both Quarkus and Micronaut could work pretty seamlessly. Micronaut needed some extra annotation in the application class we added before, to know it had to process the @Entity class. But besides that, the pure MicroProfile code was processed properly.
Spring, on the other hand, had issues with what a @Singleton means. Which was pretty fixed by using one of the specific qualifiers from Spring:
// Spring doesn't like singletones @Singleton
@Named("dbResource")
public class DatabaseResource {
Quarkus | Spring | Micronaut |
😊 | 😵 | 😵 |
Conclusion
Well, my experiment was a success. Although it was not a 100% success. Both Spring and Micronaut needed small adjustments on the code and the configuration to be able to run.
But nonetheless, MicroProfile appears to be the right way to develop in Java. The code is mostly the same in all MicroProfile frameworks, and developers should have no problem jumping from one to another. We can all read each others code and understand it. Which is not a minor thing. Each framework is implementing and experimenting on their own, but all efforts are going in the same common direction, keeping semantics and concepts common for all Java developers.
And if you are a MicroProfile purist, you can rely on Quarkus.
This article was originally a talk I delivered in the JChampions Conference in which I did a bit of live coding.