This article is originaly published on 2019-12-20 on Java Advent Calender at https://www.javaadvent.com/2019/12/intro-to-micronaut.html
Java was created in world different from the one today, at that time long running applications were common way and preferred way of working. Frameworks from that time would do all kind of stuff during startup, like scanning class-path, creating proxies, dependency injection as so on. This of course had result of longer startup time, however since application were suppose to run for long time this initial hit didn't matter. For all that magic we also needed additional memory, again taken into account that applications were running on powerful machines this didn't matter that much.
Fast forward today, and we arrive into world of micorservices and instant feedback for end users, where things as startup time and memory foot print matter a lot, due to need to scale up and down depending on the current load. Micronaut is "new" java framework, build by people who have experience in building older type of frameworks, they designed Micronaut with goal of keeping developer productivity, while addressing all drawbacks of previous frameworks under modern day requirement.
This is achieved in a very simple way. All the magic, like scanning class-path, creating proxies, dependency injection and so on that frameworks were doing in run time (startup), in Micronaut are done during compile time. This has direct result of faster startup time and lower memory footprint.
Some frameworks have so called starter kits or websites, which can help you in setting up skeleton of a project. In order to use this in case of Micronaut, you need to install micronaut on your machine, and to do so first you need to install SDKMan.
Once you install SDKMan you can install Microanut using this command
$ sdk install micronaut
After this you can use micoranut command line tool to create skeleton application, for example something like this
$ mn create-app <app name> --build maven
Let us create first simple project which will have access to DB. We will do so by running this command
$ mn create-app intromicro --build maven
with option -features we can add support out of the box for Hibernate, security and so on, however in this example we will add all dependencies manually.
In our example we will use H2 as DB, so we need to add dependency for H2
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
and also dependencies for DB layers
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-hibernate-jpa</artifactId>
<version>1.0.0.M5</version>
</dependency>
<dependency>
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-hibernate-jpa</artifactId>
<exclusions>
<exclusion> <!-- declare the exclusion here -->
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-hibernate-jpa-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>runtime</scope>
</dependency>
We also need to extend annotationProcessorPaths of compile stage and test stage with this
<annotationProcessorPaths>
.....
<path>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-processor</artifactId>
<version>1.0.0.M5</version>
</path>
......
</annotationProcessorPaths>
Last but not the least we also need to update property file. Let us add this to application.yml file
---
datasources:
default:
url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
driverClassName: org.h2.Driver
username: sa
password: ''
schema-generate: CREATE_DROP
dialect: H2
jpa:
default:
properties:
hibernate:
bytecode:
provider: none
hbm2ddl:
auto: update
Now that we have skeleton Micronaut application, let us build on top of it.
We are going to build very simple book store, with simple CRUD Rest API that is secured.
Code so far can be found here - https://github.com/vladimir-dejanovic/intro-to-micronaut-javaadvent-blogpost/tree/master/step1
Let us create entity that will host all values for Books. Book class will be simple pojo with three fields id, title and author.
public class Book {
private Long id;
private String title;
private String author;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
Let us make this pojo to an Entity in same way you would do it in any other framework. Add Entity annotation, at top of class and appropriate annotations in class it self.
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
.....
}
Next thing that we need is repository. Thanks to all dependencies that we added it will be very easy task. We just need to create interface as shown in this code
import io.micronaut.data.repository.CrudRepository;
import io.micronaut.data.annotation.Repository;
import xyz.itshark.blog.javaadvent.intromicro.pojo.Book;
@Repository
public interface BookRepository extends CrudRepository<Book,Long> {
}
As you can see, there is very little that we needed to do, we just needed to add annotation Repository, extend CrudRepository and provide classes for Entity and Primary Key, in our case Book and Long.
With this code, everything is taken care of DB, Entity and Repository for accessing DB.
Code so far can be found here - https://github.com/vladimir-dejanovic/intro-to-micronaut-javaadvent-blogpost/tree/master/step2
Let us now create REST API
One thing that I learned over the years is that you don't need to have Services for you code to work, however it helps with maintainability and usability of code especially on bigger products. So let us create one very simple Book Service.
@Singleton
public class BookService {
private BookRepository bookRepository;
public BookService( BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public Optional findById(Long id) {
return bookRepository.findById(id);
}
public List findAll() {
return bookRepository.findAll();
}
public Book addBook(Book book) {
return bookRepository.save(book);
}
}
We added annotation Singleton on class level. As you might guessed this let Micronaut know that this class will be singleton, only one.
Next thing that we will look is dependency injection. There are multiple ways of using dependency injection in Micronaut, in this example we are using dependency injection over Constructor. In our case BookRepository will be injected into our BookService once BookService is created.
Your IDE will probably complain about signature of bookRepository.findAll, so let us fix that. We need to add this code to BookRepository interface
@Override
List findAll();
Now that this is fixed, we can focus on adding REST API. Let us create class BookRestController.
Once it is created add annotation Controller to let Micronaut know this will be Controller class in so called MVC pattern. Setup root path for our controller as "/book", argument passed to annotation. Then we need to inject BookService over constructor, in similar way we did it in previous example
@Controller("/books")
public class BookRestController {
private BookService bookService;
public BookRestController(BookService bookService) {
this.bookService = bookService;
}
}
Once all of this is done, let us add some rest end points.
First let us add endpoint to retrieve all books in our book store.
@Get("/")
public List getAllBooks() {
return bookService.findAll();
}
Here we are registering code to react on GET request to "/books/", remember that we added root path for Controller to /books/.
Next let us add endpoint to retrieve single Book record for corresponding ID.
@Get("/{id}")
public Book getBookById(@PathVariable("id") Long id) {
return bookService.findById(id).orElse(null);
}
Compare to previous example, here we added PathVariableannotation to method signature and also we added "{id}" as Get annotation argument.
Last end point that we will added to our REST API is end point to add data.
@Post("/")
public Book addBook(@Body Book book) {
return bookService.addBook(book);
}
Of course we are using Post method, and also are expecting for data load to be part of request body. That is why used Post annotation and Body annotation in method signature.
Let us now test if all is working as expected. If we hit /books/ end point with GET method we expect to get empty list.
$ curl http://localhost:8080/books/
[]
All good so far, let us now try to add some data, for that we will hit same URL with POST method.
$ curl -X POST -d '{"title":"1Q84","author":"Haruki Murakami"}' -H 'Content-type:application/json' http://localhost:8080/books/
{"id":1,"title":"1Q84","author":"Haruki Murakami"}
$ curl -X POST -d '{"title":"Dance Dance Dance","author":"Haruki Murakami"}' -H 'Content-type:application/json' http://localhost:8080/books/
{"id":2,"title":"Dance Dance Dance","author":"Haruki Murakami"}
If we call same end point again with GET we expect to get data that we have just put in
$ curl http://localhost:8080/books/
[{"id":1,"title":"1Q84","author":"Haruki Murakami"},{"id":2,"title":"Dance Dance Dance","author":"Haruki Murakami"}]
Let us now request book by id
$ curl http://localhost:8080/books/2
{"id":2,"title":"Dance Dance Dance","author":"Haruki Murakami"}
Code so far can be found here - https://github.com/vladimir-dejanovic/intro-to-micronaut-javaadvent-blogpost/tree/master/step3
Now that we added basic application, let us secure it.
First we need to add additional dependency
io.micronaut
micronaut-security-session
compile
Next we need to update application.yml
---
micronaut:
security:
enabled: true
endpoints:
login:
enabled: true
logout:
enabled: true
session:
enabled: true
loginSuccessTargetUrl: /
loginFailureTargetUrl: /
To secure our end points we can just add annotation Secured. In order to make sure only authenticated users can add data to our app we need to add @Secured(SecurityRule.IS_AUTHENTICATED) to addBook method.
@Post("/")
@Secured(SecurityRule.IS_AUTHENTICATED)
public Book addBook(@Body Book book) {
return bookService.addBook(book);
}
In order to allow users to still get info about books without any authentication we need to add @Secured(SecurityRule.IS_ANONYMOUS)
@Get("/")
@Secured(SecurityRule.IS_ANONYMOUS)
public List getAllBooks() {
return bookService.findAll();
}
@Get("/{id}")
@Secured(SecurityRule.IS_ANONYMOUS)
public Book getBookById(@PathVariable("id") Long id) {
return bookService.findById(id).orElse(null);
}
There are multiple ways to add security inside Micronaut, using annotations is one of them.
Next thing that we need to provide is authentication provider, which we will use to authenticate users.
Let us create class SimpleAuthenticationProvider which implements AuthenticationProvider. This class should also be singleton. Since this is just simple example we can create dummy authentication which is expecting for username to be user and password to be password.
@Singleton
public class SimpleAuthenticationProvider implements AuthenticationProvider {
@Override
public Publisher authenticate(AuthenticationRequest authenticationRequest) {
if(authenticationRequest.getIdentity().equals("user") && authenticationRequest.getSecret().equals("password")) {
return Flowable.just(new UserDetails("user",new ArrayList()));
} else return Flowable.just(new AuthenticationFailed());
}
}
If we try again to insert data using post it should fail now
$ curl -v -X POST -d '{"title":"1Q84","author":"Haruki Murakami"}' -H 'Content-type:application/json' http://localhost:8080/books/
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /books/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.52.1
> Accept: */*
> Content-type:application/json
> Content-Length: 43
>
* upload completely sent off: 43 out of 43 bytes
< HTTP/1.1 401 Unauthorized
< Date: Sat, 14 Dec 2019 17:27:31 GMT
< transfer-encoding: chunked
< connection: close
<
* Curl_http_done: called premature == 0
* Closing connection 0
as we can see, we are not authorized for this action. Let us try again, but this time provide username and password
$ curl -X POST -d '{"title":"After Dark","author":"Haruki Murakami"}' -H 'Content-type:application/json' http://localhost:8080/books/ -u 'user:password'
{"id":3,"title":"After Dark","author":"Haruki Murakami"}
let us check if reading still works without credentials
$ curl http://localhost:8080/books/
[{"id":1,"title":"1Q84","author":"Haruki Murakami"},{"id":2,"title":"Dance Dance Dance","author":"Haruki Murakami"},{"id":3,"title":"After Dark","author":"Haruki Murakami"}]
Code so far can be found here - https://github.com/vladimir-dejanovic/intro-to-micronaut-javaadvent-blogpost/tree/master/step4
As you can see developing applications in Micronaut is very productive from Developer point of view, beside this you get a lot of goodies out of the box from vast Micronaut ecosystem, and also will benefit from the start with faster startups, lower memory foot print, reactive systems and much more.
Remember what we covered in this article is just a small part of Micronaut and benefits you get from it.