There is a new microservices framework in town called
Micronaut. In this post, I'll discuss how I've ventured into working with the Micronaut framework from a Java EE/Jakarta EE perspective. I am a Java EE developer, so developing microservices using solutions such as
Eclipse MicroProfile are closer to my line of expertise, but Micronaut caught my attention since it offers the following abilities:
- Develop in Java, Groovy, or Kotlin
- Easy to test..fully integrated testing with Spock or JUnit
- Embedded Server & Compile Time HTTP Client
- Easy packaging for Docker
- Fast startup time, low memory consumption
- Fully Reactive
As an enterprise developer at heart, my first thought usually goes to the database, as the majority of applications that I author utilize an RDBMS. I found the number of examples using Micronaut with an RDBMS to be few and far between, so I thought it may be useful for me to create another example for that use case. In this example, I utilize PostgreSQL. However, most other RDBMS are also supported. This article is not meant to be a full explanation of installing Micronaut or utilizing all of the many Micronaut features. Instead, it is a primer for those looking to get started utilizing Micronaut with a relational database...particularly geared towards those with some Java EE/Jakarta EE background.
In my particular case, I am interested in quickly spinning up Microservices that are fully testable, extensible, and efficient. Although I can do this with MicroProfile or standard Java EE, I thought it would be interesting to learn something new and also have the ability to utilize Groovy or Kotlin. I also wanted to put a Java EE/Jakarta EE spin on it...so I'm using JPA for working with the data. Many of the Micronaut examples utilize Groovy and GORM for persistence...but I likely wouldn't be using that in any of my applications.
The example was developed using
Apache NetBeans 9.0 and the
Command Line Interface (CLI) that comes packaged with Micronaut. This particular example was written against Micronaut 1.0.0.M4. In this case, I kept it simple and utilized only a single, basic database table for persistence within a PostgreSQL database.
To begin, I created an app by utilizing the CLI by issuing the following command:
mn create-app org.acme.books --features hibernate-jpa,jdbc-tomcat
This simply creates a skeleton for my app within a directory named "books", and the
Application.java main class will be placed within the
org.acme.books package. By default, there are basic features supported by an application, but in this case I've added support for the Tomcat connection pool. This will be utilized when creating database connections via the Java Persistence API (JPA). The default application is also generated with support for the Gradle build system. Therefore, a build.gradle is created, and that is the file in which dependency management will take place. Note that an application can also be generated utilizing the Apache Maven build system, but I had issues running Maven projects under Micronaut 1.0.0.M4...so I stuck with Gradle for this example.
If using Apache NetBeans 9.0, you can install the "Groovy and Grails" and "Gradle" plugins (currently available in the NetBeans 8.2 plugin center) to provide support for opening the project. Once this is completed, the project can be opened within NetBeans and development can begin. After installing the plugins and opening the project within Apache NetBeans, the completed project structure should look like that in the following figure:
To provide support for the PostgreSQL database, I added the dependencies to build.gradle:
compile group: 'org.postgresql', name: 'postgresql', version: '42.2.5'
Next, I opened up the application.yml file and added a datasource for the application. This is the file that takes place of a persistence.xml within a traditional Java EE application. Also, JPA support is added via this file, indicating which package includes the entity classes, as well as configuration of Hibernate. Port 8080 is also set, as by default Micronaut will choose a random port on which to start the server. The full sources of application.xml are as follows:
micronaut:
application:
name: books
#Uncomment to set server port
server:
port: 8080
---
datasources:
default:
url: jdbc:postgresql://localhost/postgres
username: postgres
password: yourpassword
driverClassName: org.postgresql.Driver
connectionTimeout: 4000
jpa:
default:
packages-to-scan:
- 'org.acme.domain'
properties:
hibernate:
hbm2ddl:
auto: update
show_sql: true
Now that the configuration is out of the way, I can get to the fun part...development. In this example, I create a basic service allowing one to create, read, update, or delete records in the BOOK table. The automatically generated Application class within the org.acme package, which starts the service.
package org.acme;
import io.micronaut.runtime.Micronaut;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class);
}
}
To begin development, create two packages within the application for organizing the source code. First, create org.acme.domain, which will contain the entity class. Next, create org.acme.book, which will contain the implementation classes. Create a Book.java class within the org.acme.domain package, which will be the entity class containing a standard JPA mapping for the database. In this case, note that I utilize java.time.LocalDate for the date fields, and I utilize a database sequence generator for population of the primary key. The sources are as follows:
package org.acme.domain;
import java.time.LocalDate;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
/**
* JPA Mappings for the BOOK database table.
*/
@Entity
@Table(name="BOOK")
public class Book {
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE,
generator="book_generator")
@SequenceGenerator(name="book_generator",sequenceName="book_s", allocationSize=1)
private Long id;
@Column(name="PUBLISH_DATE")
@NotNull
private LocalDate publishDate;
@Column(name="TITLE")
@NotNull
private String title;
@Column(name="AUTHOR_FIRST")
@NotNull
private String authorFirst;
@Column(name="AUTHOR_LAST")
@NotNull
private String authorLast;
private Long pages;
public Book(){}
public Book(@NotNull Long id, @NotNull LocalDate publishDate, @NotNull String title, String authorFirst, String authorLast, Long pages){
this.id = id;
this.publishDate = publishDate;
this.title = title;
this.authorFirst = authorFirst;
this.authorLast = authorLast;
this.pages = pages;
}
public Book(@NotNull LocalDate publishDate, @NotNull String title, String authorFirst, String authorLast, Long pages){
this.publishDate = publishDate;
this.title = title;
this.authorFirst = authorFirst;
this.authorLast = authorLast;
this.pages = pages;
}
/**
* @return the id
*/
public Long getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Long id) {
this.id = id;
}
/**
* @return the publishDate
*/
public LocalDate getPublishDate() {
return publishDate;
}
/**
* @param publishDate the publishDate to set
*/
public void setPublishDate(LocalDate publishDate) {
this.publishDate = publishDate;
}
/**
* @return the title
*/
public String getTitle() {
return title;
}
/**
* @param title the title to set
*/
public void setTitle(String title) {
this.title = title;
}
/**
* @return the authorFirst
*/
public String getAuthorFirst() {
return authorFirst;
}
/**
* @param authorFirst the authorFirst to set
*/
public void setAuthorFirst(String authorFirst) {
this.authorFirst = authorFirst;
}
/**
* @return the authorLast
*/
public String getAuthorLast() {
return authorLast;
}
/**
* @param authorLast the authorLast to set
*/
public void setAuthorLast(String authorLast) {
this.authorLast = authorLast;
}
/**
* @return the pages
*/
public Long getPages() {
return pages;
}
/**
* @param pages the pages to set
*/
public void setPages(Long pages) {
this.pages = pages;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", publishDate='" + publishDate + '\'' +
", title='" + title + '\'' +
", authorFirst='" + authorFirst + '\'' +
", authorLast='" + authorLast + '\'' +
", pages='" + pages +
'}';
}
}
In a Micronaut application, HTTP requests and responses need to be encapsulated in Serializable classes for processing, and therefore it makes sense to generate some simple "Plain Old Java Objects" (POJOs) for encapsulating the data that will be used within database operations. In the same org.acme.domain package, I created two such classes, BookSaveOperation.java and BookUpdateOperation.java. These classes will define the fields required for passing data from the HTTP request to the controller class. The sources for BookSaveOperation.java are as follows (see the GitHub repository for full sources):
package org.acme.domain;
import java.time.LocalDate;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
*
* @author Josh Juneau
*/
public class BookSaveOperation implements java.io.Serializable {
@NotNull
private LocalDate publishDate;
@NotNull
@NotBlank
private String title;
@NotNull
@NotBlank
private String authorFirst;
@NotNull
@NotBlank
private String authorLast;
private Long pages;
public BookSaveOperation(){}
public BookSaveOperation(LocalDate publishDate, String title,
String authorFirst, String authorLast, Long pages){
this.publishDate = publishDate;
this.title = title;
this.authorFirst = authorFirst;
this.authorLast = authorLast;
this.pages = pages;
}
// ...
// getters and setters
// ...
}
The application business logic occurs within a class which is much like an EJB or DAO implementation, and the class must implement an interface that has defined each of the business logic methods. In this case, I created an interface org.acme.book.BookRepository.java, and define a few standard operational methods:
package org.acme.book;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.acme.domain.Book;
/**
*
*/
public interface BookRepository {
Book save(LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages);
Optional<Book> findById(Long id);
void deleteById(Long id);
List<Book> findAll();
int update(Long id, LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages);
}
Next, implement that interface within a class entitled org.acme.book.BookRepositoryImpl.java, and annotate as a @Singleton. Since this is the class which will implement business logic, inject a PersistenceContext, which provides the JPA EntityManager that will be used for performing database operations. Simply implement each of the operations outlined within the BookRepository interface, marking each with @Transactional (io.micronaut.spring.tx.annotation.Transactional), implying read only for those methods that will not modify any data. The sources for BookRepositoryImpl.java are as follows:
package org.acme.book;
import io.micronaut.configuration.hibernate.jpa.scope.CurrentSession;
import io.micronaut.spring.tx.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.acme.domain.Book;
/**
* Business logic for the service.
*/
@Singleton
public class BookRepositoryImpl implements BookRepository {
@PersistenceContext
private EntityManager entityManager;
public BookRepositoryImpl(@CurrentSession EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
@Transactional
public Book save(LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages) {
Book book = new Book(publishDate, title, authorFirst, authorLast, pages);
entityManager.persist(book);
return book;
}
@Override
@Transactional(readOnly = true)
public Optional<Book> findById(Long id) {
return Optional.ofNullable(entityManager.find(Book.class, id));
}
@Transactional(readOnly = true)
public List<Book> findAll() {
return entityManager
.createQuery("SELECT b FROM Book b", Book.class)
.getResultList();
}
@Override
@Transactional
public int update(Long id, LocalDate publishDate, String title, String authorFirst, String authorLast, Long pages) {
return entityManager.createQuery("UPDATE Book b SET publishDate = :publishDate, title = :title, " +
"authorFirst = :authorFirst, authorLast = :authorLast, pages = :pages where id = :id")
.setParameter("publishDate", publishDate)
.setParameter("title", title)
.setParameter("authorFirst", authorFirst)
.setParameter("authorLast", authorLast)
.setParameter("pages", pages)
.setParameter("id", id)
.executeUpdate();
}
@Override
@Transactional
public void deleteById(Long id) {
findById(id).ifPresent(book -> entityManager.remove(book));
}
}
In an effort to explain the Micronaut application infrastructure from a Java EE perspective, I'll compare the implementation with a simple JAX-RS application. Micronaut utilizes io.micronaut.http.annotation.Controller classes to perform the request-response handling for a service. This is much like a JAX-RS controller class, with a few slight differences. This very much reminds me of the
Eclipse Krazo project, or MVC 1.0 for Java EE. For instance, instead of annotating methods with the JAX-RS annotations javax.ws.rs.GET, javax.ws.rs.POST, or javax.ws.rs.Path, Micronaut uses io.micronaut.http.annotation.Get and io.micronaut.http.annotation.Post, among others. The URI path for each of the methods can be directly declared via the
@Get,
@Post,
@Put,
@Delete annotations. Each controller class will implement the functionality for the service and handles the request-response life cycle. The business logic for persistence (contained within the
BookRepositoryImpl class) is injected into the controller class via the
@Inject annotation or via constructor injection. In the sources for this example, constructor injection is used.
package org.acme.book;
import org.acme.domain.Book;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import io.micronaut.validation.Validated;
import javax.validation.Valid;
import java.net.URI;
import java.util.List;
import org.acme.domain.BookSaveOperation;
import org.acme.domain.BookUpdateOperation;
@Validated
@Controller("/books")
public class BookController {
protected final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Get("/")
public List<Book> list() {
return bookRepository.findAll();
}
@Put("/")
public HttpResponse update(@Body @Valid BookUpdateOperation operation) {
bookRepository.update(operation.getId(), operation.getPublishDate(),
operation.getTitle(), operation.getAuthorFirst(), operation.getAuthorLast(), operation.getPages());
return HttpResponse.noContent().header(HttpHeaders.LOCATION, location(operation.getId()).getPath());
}
@Get("/{id}")
Book show(Long id) {
return bookRepository
.findById(id)
.orElse(null);
}
@Delete("/{id}")
HttpResponse delete(Long id) {
bookRepository.deleteById(id);
return HttpResponse.noContent();
}
@Post("/")
HttpResponse<Book> save(@Body @Valid BookSaveOperation operation) {
Book book = bookRepository.save(operation.getPublishDate(), operation.getTitle(),
operation.getAuthorFirst(), operation.getAuthorLast(), operation.getPages());
return HttpResponse
.created(book)
.headers(headers -> headers.location(location(book)));
}
protected URI location(Book book) {
return location(book.getId());
}
protected URI location(Long id) {
return URI.create("/books/" + id);
}
}
Testing the Application
Micronaut provides easy testing with Spock or JUnit and an embedded server...making it easy to create tests for each of the controllers. In this case, I utilize JUnit to test the application. I created a testing class within the test folder of the project named org.acme.BookControllerTest.
package org.acme;
import io.micronaut.context.ApplicationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import org.acme.domain.Book;
import org.acme.domain.BookSaveOperation;
import org.acme.domain.BookUpdateOperation;
import org.junit.AfterClass;
import static org.junit.Assert.assertEquals;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Test cases for BookController
*/
public class BookControllerTest {
private static EmbeddedServer server;
private static HttpClient client;
private Book book;
HttpRequest request;
HttpResponse response;
Long id;
List<Long> bookIds = new ArrayList<>();
@BeforeClass
public static void setupServer() {
server = ApplicationContext.run(EmbeddedServer.class);
client = server.getApplicationContext().createBean(HttpClient.class, server.getURL());
}
@AfterClass
public static void stopServer() {
if (server != null) {
server.stop();
}
if (client != null) {
client.stop();
}
}
@Test
public void testInsertBooks() {
request = HttpRequest.POST("/books", new BookSaveOperation(LocalDate.now(), "Java EE 8 Recipes", "Josh", "Juneau", new Long(750)));
response = client.toBlocking().exchange(request);
assertEquals(HttpStatus.CREATED, response.getStatus());
request = HttpRequest.POST("/books", new BookSaveOperation(LocalDate.now(), "Java 9 Recipes", "Josh", "Juneau", new Long(600)));
response = client.toBlocking().exchange(request);
id = entityId(response, "/books/");
assertEquals(HttpStatus.CREATED, response.getStatus());
}
@Test
public void testBookRetrieve() {
request = HttpRequest.GET("/books");
List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class));
// Populate a book instance for later
for(Book b:books){
book = b;
}
assertEquals(2, books.size());
}
@Test
public void testBookOperations() {
request = HttpRequest.GET("/books");
List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class));
// Populate a book instance for later
for(Book b:books){
book = b;
}
request = HttpRequest.PUT("/books/", new BookUpdateOperation(book.getId(),
book.getPublishDate(),
"Java 10 Recipes",
book.getAuthorFirst(),
book.getAuthorLast(),
book.getPages()));
response = client.toBlocking().exchange(request);
assertEquals(HttpStatus.NO_CONTENT, response.getStatus());
request = HttpRequest.GET("/books/" + book.getId());
book = client.toBlocking().retrieve(request, Book.class);
assertEquals("Java 10 Recipes", book.getTitle());
testDelete();
}
public void testDelete(){
request = HttpRequest.GET("/books");
List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class));
// Populate a book instance for later
for(Book b:books){
request = HttpRequest.DELETE("/books/" + b.getId());
response = client.toBlocking().exchange(request);
assertEquals(HttpStatus.NO_CONTENT, response.getStatus());
}
}
Long entityId(HttpResponse response, String path) {
String value = response.header(HttpHeaders.LOCATION);
if (value == null) {
return null;
}
int index = value.indexOf(path);
if (index != -1) {
return Long.valueOf(value.substring(index + path.length()));
}
return null;
}
}
Walk-Through of the Logic in Testing Class
In the method that runs @BeforeClass, the HTTP server and client are created. Similarly, when the tests have finished executing, the method that is annotated with @AfterClass is invoked, which stops the server if it is running.
From within the textInsertBooks() method, two new book records are created by passing new BookSaveOperation objects populated with data to the service that is available via the "/books" path with the @Post designation. In this case, the controller method BookController.save() is invoked. Taking a look at the save() method, you can see that the method simply passes the contents of BookSaveOperation to the BookRepository.save() business method (utilizing the interface), persisting the object. In the end, an HttpResponse is returned.
The testBookRetrieve() method calls upon the service that is available via the "/books" path containing the @Get designation. This, in-turn, calls upon the BookController.list() method, which performs a findAll() on the BookRepository, returning a List of Book objects.
The testBookOperations() method is responsible for performing updates to the records. First, the list of Book objects is retrieved from the BookController, and then one of the books is updated via the BookController.update() method by populating a BookUpdateOperation object with the contents to be updated.
** Keep in mind, the BookSaveOperation.java and BookUpdateOperation.java objects are simply POJOs that are used to move the data
Lastly, the testDelete() method is invoked, which traverses the List of Book objects, calling upon the BookController.delete() method via the service call to the "/books" path and invoking the method designated as @Delete.
To execute the tests, simply right-click the project in NetBeans and choose "Test", or use the command line to invoke using the following command
./gradlew test
If the database table has not yet been created, then it will be generated for you. Note that you may need to modify the database configuration within application.yml accordingly for your environment.
Running the Service
Micronaut is self-contained, allowing a service to be executed using the embedded server which is built on Netty. This can be done by right-clicking the project in Apache NetBeans and selecting "Run". You could also go to the command line and invoke using the following command:
./gradlew run
You should see the URL on which the server is hosted displayed within the output in the terminal or Apache NetBeans output panel.
Summary
As a Java EE developer, I have to say that Micronaut does have a different development approach. It is not too much different than that of Spring Boot or Grails, but it is different enough that it took me some time to find my way around. In the end, I find it an easy to use framework that has a lot of potential for quickly developing services or scheduled tasks, while still harnessing some of the concepts from Java EE/Jakarta EE.
There are a number of features that I've not played much with yet, such as creating scheduled tasks via the Micronaut framework, and developing using Groovy or Kotlin, rather than Java. I hope to follow up to this post in the future with more information for Java EE and Jakarta EE developers who are interested in beginning their journey with Micronaut.