java -jar openapi-generator-cli-XXX.jar help
Table of Contents
- 1. Getting Started
- 2. What you will need
- 3. Installing OpenAPI Generator
- 4. Creating The API Definition File
- 5. Generating Server API From The OpenAPI Definition
- 6. Application Structure
- 7. Data Storage And Access With MySQL and JDBC
- 8. Writing the Controller Logic
- 9. Test Resources
- 10. Running the Application
- 11. Testing the Application
- 12. Generate a Micronaut Application Native Executable with GraalVM
- 13. Next steps
Micronaut Server generation with OpenAPI
Learn how to write an OpenAPI definition, use it to generate a server template for a Micronaut application, and get it all to work
Authors: Andriy Dmytruk
Micronaut Version: 3.9.2
1. Getting Started
In this guide, we will write an OpenAPI definition file and then use it to generate a Java Micronaut server API with OpenAPI Generator.
Then we will add internal logic to the API and test our implementation.
1.1. What OpenAPI Is
The OpenAPI Specification defines a format for uniquely describing REST APIs that is both human- and machine-readable. Later in this guide, we will discover the structure of documents in the OpenAPI format. We will also create such a document for our desired API. Note that we will refer to the document describing our API as an API definition file.
1.2. Advantages Of OpenAPI
-
It provides a unique way of describing a REST API that is easy to understand and modify. It is the most broadly adopted industry standard for describing new APIs and has the most developed tooling ecosystem.
-
You can generate interactive documentation and client implementations from the same definition file in numerous languages.
-
You can use the same definition file to generate a server template. The template will include client-server communication specifics based on your API definition. It removes the need for developers to write extensive documentation about each possible path and parameter for the APIs - most can be described in the definition file. This prevents incompatibility issues between the client and server sides which might be caused by ill-communication.
The internal server logic cannot be generated from a definition file and needs to be implemented manually based on the generated server template. The reason for this is very simple: there cannot be a unified way of describing all the possible server implementations.
1.3. What The OpenAPI Generator Is
OpenAPI Generator allows the generation of API client libraries (SDK generation), server stubs, documentation, and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported).
1.4. What You Will Learn
-
You will discover the general structure of a document in the OpenAPI format and a definition file in this format describing the desired API for our custom server.
-
You will learn to use the OpenAPIGenerator to generate Micronaut code in Java for the server application. We will extend the code by implementing internal logic and testing it.
-
You will learn how to use Micronaut Data JDBC to connect to a MySQL database from our application to store and retrieve data. You will complement the application with tests.
1.5. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can directly get the complete solution by downloading and unzipping micronaut-openapi-generator-server-maven-java.zip.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
3. Installing OpenAPI Generator
To use OpenAPI Generator, we need to install OpenAPI Generator CLI.
We will use the jar option. Download it and store it in the directory that you want to use for this project.
Next, open terminal in the same directory. To verify that generator works correctly run the help
command:
It should provide a description and a list of commands.
All the options for installation are given at the "CLI Installation" guide on the OpenAPI Generator website. In particular:
If you installed the generator with a package manager or bash launcher script, simply run
|
You can also use the OpenAPI Generator Online Service but its usage is not covered by this guide. |
4. Creating The API Definition File
We will now create a definition file that will describe our server API, including the available paths and operations.
The definition file must be in the OpenAPI format. The document must have a specific structure. "OpenAPI Specification" guide describes it with more detail. We will write sections of the definition document based on the specification.
OpenAPI generator supports .yaml
and .json
file formats for the definition file. We will use YAML due to its simplicity and human readability.
In the directory where you downloaded the OpenAPI generator CLI, create a file named library-definition.yaml
and open it in your favourite text editor.
4.1. Describing General Server Info
We will first provide general server information in the definition file. Paste the following text to the file:
openapi: 3.0.0 (1)
info: (2)
description: This is a library API
version: 1.0.0
title: Library
license:
name: Apache-2.0
url: "https://www.apache.org/licenses/LICENSE-2.0.html"
tags: (3)
- name: books
description: Search for books and add new ones
1 | The version that will be used for parsing. |
2 | The info object contains general information about the API like a short description and license. In this case, we will be creating a website for a library. |
3 | Tags will be used to logically structure different paths. |
If you are new to OpenAPI, you might be interested in reading the OpenAPI guide or the OpenAPI 3.0.0 specification after you finish this guide. |
4.2. Defining Paths And Operations
The paths section of the definition is described in the "API Endpoints" OpenAPI Guide, but can also be understood from a few examples. This section defines paths and various operations (like GET
, PUT
, and POST
) available on these paths.
We will proceed by defining a path that is supposed to be used for searching books in our library. The parameters that we will define in the definition will be used to narrow the search results.
Paste the following to our file:
paths:
/search:
get: (1)
tags:
- books (2)
summary: Search for a book
operationId: search (3)
parameters: (4)
- name: book-name
in: query
schema:
type: string
minLength: 3 (5)
- name: author-name
in: query
schema:
type: string
responses: (6)
"200": (7)
description: Success
content:
"applicaton/json":
schema:
type: array,
items:
$ref: "#/components/schemas/BookInfo"
"400": (8)
description: Bad Request
1 | We define the GET operation on the /search path. |
2 | We use the books tag that we previously defined. Note that for each tag a controller will be generated that will implement its operations. |
3 | The search operation id will be used as method name for the given path. |
4 | We define two parameters of type string that the user should supply in the query. |
5 | Validation can be used on parameters. In this case, book name must contain at least three characters. |
6 | The responses object describes the response codes that can be produced. It also defines the structure of body if any. |
7 | In case of correct request, we define the body to contain a list of BookInfo objects. The schema for the book info object will be defined later in components/schemas section of the definition. |
8 | The "400" status code will be produced by Micronaut in case of a bad request, like an incorrect type supplied or failed validation. Even though Micronaut handles it automatically and no implementation is needed on our side, we add it for a complete API specification. |
You can read more about parameter descriptions in the "Describing Parameters" OpenAPI guide. All the available types and their validations are described in "Data Models (Schemas)" OpenAPI guide. |
We will define another path with a POST
operation, that is supposed to be used to add info about a book in our library. In this case, the request will contain a body with all the book information:
/add:
post: (1)
tags: [books]
summary: Add a new book
operationId: addBook
requestBody: (2)
required: true
content:
"application/json":
schema:
$ref: "#/components/schemas/BookInfo" (3)
responses:
"200":
description: Success
"400":
description: Bad Request
1 | We define the POST method for the /add path, and add the same tag books to it. |
2 | We specify that a body is required and what are the supported content-types for it. (in this case only application/json , but multiple can be allowed). |
3 | We write that BookInfo object is required to be in the request body. We reference the same BookInfo schema that we will define next. |
To read more about body definitions, see the "Describing Request Body" OpenAPI guide. |
4.3. Defining Schemas
Schemas are required whenever a parameter, request body or a response body we want to describe needs to be an object. In that case we add a schema that defines all the properties of the object. You can find out about the format for schemas in the "Content of Message Bodies" OpenAPI Guide.
We will add schemas to our definition file:
components:
schemas:
BookInfo:
title: Book Info (1)
description: Object containg all the info about a book
type: object
properties: (2)
name: {type: string}
availability: {$ref: "#/components/schemas/BookAvailability"} (3)
author: {type: string, minLength: 3}
ISBN: {type: string, pattern: "[0-9]{13}"}
required: ["name", "availability"]
BookAvailability: (4)
type: string
enum: ["available", "not available", "reserved"]
1 | We define the BookInfo schema inside then components/schemas section. From this schema a Java class will be generated with the same BookInfo class name. |
2 | We define all the properties of BookInfo , including required validation on them (In this case, it is a minimal length requirement on one string and a regex pattern on another). An abbreviated form is used for some YAML lists and dictionaries to reduce the number of rows and simplify readability. |
3 | We reference another schema to be used as a property. |
4 | We define BookAvailability schema to be an enum with three available values. A Java BookAvailability class will be generated with given enum values based on our definition. |
As you can see, schemas can be defined as enums when they can only be assigned a finite number of values. Also, you can reference other schemas as properties of a schema.
You can read more about writing schemas in the "Data Models (Schemas)" OpenAPI guide. |
Save the file and proceed to the next part of the guide.
5. Generating Server API From The OpenAPI Definition
Now we will generate server API files from our definition. The generated server code will be in Java and will use the Micronaut features for client-server communication.
Open the terminal in the same directory as library-definition.yaml
file and run the following command:
java -jar openapi-generator-cli-XXX.jar generate \
-g java-micronaut-server \(1)
-i library-definition.yaml \(2)
-o ./ \(3)
-p controllerPackage=example.micronaut.controller \(4)
-p modelPackage=example.micronaut.model \(5)
-p build=maven \(6)
-p test=junit(7)
1 | Specify that we will use Java Micronaut server generator. |
2 | Specify our OpenAPI definition file as library-definition.yaml , which we just created. |
3 | Specify the output directory to be the current directory (./ ).
You can specify it to be a different one if you want (e.g. library-server ). |
4 | We provide generator-specific properties starting with -p .
We want all the controllers to be generated in the example.micronaut.controller package. |
5 | We want all the models (data models, like BookInfo ) to be in example.micronaut.model package. |
6 | We want to use maven as build tool. The supported values are gradle , maven and all .
If nothing is specified, both Maven and Gradle files are generated. |
7 | We want to use JUnit 5 for testing. The supported values are junit (JUnit 5) and spock .
If nothing is specified, junit is used by default. |
If you want to view all the available parameters for micronaut server generator, run
|
If you plan to change the definition file and regenerate files, consider setting the |
After running, the OpenAPI generator CLI will output information about generated files. Now you can open the directory in your favorite IDE or text editor.
You should see the following directory structure:
./
├── docs
│ └── ... (1)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── example/micronaut/
│ │ │ ├── Application.java (2)
│ │ │ ├── controller
│ │ │ │ └── BooksController.java (3)
│ │ │ └── model
│ │ │ ├── BookAvailability.java (4)
│ │ │ └── BookInfo.java
│ │ └── resources/
│ │ ├── application.yml (5)
│ │ └── logback.xml
│ └── test/
│ └── java/
│ └── example/micronaut/ (6)
│ ├── controller
│ │ └── BooksControllerTest.java
│ └── model
│ ├── BookAvailabilityTest.java
│ └── BookInfoTest.java
├── README.md
└── ...
1 | The docs/ directory contains automatically generated Markdown files with documentation about your API. |
2 | Starts the Micronaut server with detected controllers. |
3 | The BooksController is generated based on paths with books tag. It is generated in the package we specified
for controllers earlier. |
4 | Two files are generated in the models/ directory based on schemas we provided in the definition. |
5 | Config file for Micronaut is generated with a default value for server port and other parameters. |
6 | Tests are generated for all the controllers and models. |
6. Application Structure
To better understand the Micronaut Application we want to develop, let’s first look at the schematic of the whole application:
1 | The controller will receive client requests utilizing Micronaut server features. |
2 | The controller will call repository methods responsible for interaction with the database. |
3 | The repository methods will be implemented utilizing Micronaut JDBC, and will send queries to the database. |
4 | The files we generated with OpenAPI generator include Micronaut features responsible for server-client communication, like parameter and body binding, and JSON conversion. |
7. Data Storage And Access With MySQL and JDBC
We will use MySQL database to store and access data. This will ensure that stored data is persistent between the server runs and can be easily accessed and modified by multiple instances of our application.
Before implementing any server logic, we need to create a database and configure a connection to it. We will use Flyway to set up the database schema and JDBC for accessing the data.
7.1. Configure Access for a Data Source
We will use Micronaut Data JDBC to access the MySQL data source.
Add the following required dependencies:
<!-- Add the following to your annotationProcessorPaths element -->
<path>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-processor</artifactId>
</path>
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-jdbc</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql-connector-java</groupId>
<artifactId>mysql</artifactId>
<scope>runtime</scope>
</dependency>
Locally, the database will be provided by Test Resources.
datasources:
default: (1)
dialect: MYSQL
driverClassName: ${JDBC_DRIVER:com.mysql.cj.jdbc.Driver} (2)
1 | Create datasource called default . |
2 | Set the dialect and driver class name. |
With the configured data source we will be able to access the data using Micronaut JDBC API, which will be shown further in the guide.
7.2. Database Migration with Flyway
We need a way to create the database schema. For that, we use Micronaut integration with Flyway.
Flyway automates schema changes, significantly simplifying schema management tasks, such as migrating, rolling back, and reproducing in multiple environments.
Add the following snippet to include the necessary dependencies:
<dependency>
<groupId>io.micronaut.flyway</groupId>
<artifactId>micronaut-flyway</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<scope>runtime</scope>
</dependency>
We will enable Flyway in application.yml
and configure it to perform migrations on one of the defined data sources.
flyway:
datasources:
default:
enabled: true (1)
1 | Enable Flyway for the default datasource. |
Configuring multiple data sources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each data source. Review the Micronaut Flyway documentation for additional details. |
Flyway migration will be automatically triggered before your Micronaut application starts. Flyway will read migration commands in the resources/db/migration/
directory, execute them if necessary, and verify that the configured data source is consistent with them.
Create the following migration files with the database schema creation:
CREATE TABLE book (
id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
name VARCHAR(255) NOT NULL,
availability ENUM('available', 'reserved', 'not available') NOT NULL,
author VARCHAR(255),
ISBN CHAR(13)
);
INSERT INTO book
(name, availability, author, ISBN)
VALUES
("Alice's Adventures in Wonderland", "available", "Lewis Caroll", "9783161484100"),
("The Hitchhiker's Guide to the Galaxy", "reserved", "Douglas Adams", NULL),
("Java Guide for Beginners", "available", NULL, NULL);
The SQL commands in the migration will create the book
table with id
and four columns describing its properties, and populate the table with three sampe rows.
7.3. Creating a MappedEntity
To retrieve objects from the database, you need to define a class annotated with @MappedEntity. Instances of the class will represent a single row retrieved from the database in a query.
We will now create BookEntity
class. We will be retrieving data from the book
table, and therefore class properties match columns in the table. Note that special annotations are added on the property corresponding to the primary key of the table.
package example.micronaut;
import example.micronaut.model.BookAvailability;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.MappedProperty;
import javax.validation.constraints.NotNull;
@MappedEntity("book") (1)
public class BookEntity {
@Id (2)
@GeneratedValue(GeneratedValue.Type.AUTO)
private Long id;
@NonNull
@NotNull
private String name;
@NonNull
@NotNull
private BookAvailability availability;
@Nullable
private String author;
@Nullable
@MappedProperty("ISBN")
private String isbn;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@NonNull
public BookAvailability getAvailability() {
return availability;
}
public void setAvailability(@NonNull BookAvailability availability) {
this.availability = availability;
}
@Nullable
public String getIsbn() {
return isbn;
}
public void setIsbn(@Nullable String isbn) {
this.isbn = isbn;
}
@NonNull
public String getName() {
return name;
}
public void setName(@NonNull String name) {
this.name = name;
}
@Nullable
public String getAuthor() {
return author;
}
public void setAuthor(@Nullable String author) {
this.author = author;
}
}
1 | Annotate the class with @MappedEntity to map the class to the table defined in the schema. |
2 | Specifies the ID of an entity |
7.4. Writing a Repository
Next, we will create a repository interface and define the required operations to access the database. Micronaut Data will implement the interface at compilation time. It will determine the operations to be implemented based on method naming and parameters, and supports simple create, read, update, delete operations along with highly-customizable queries.
package example.micronaut;
import example.micronaut.model.BookAvailability;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.GenericRepository;
import io.micronaut.data.repository.jpa.JpaSpecificationExecutor;
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
@JdbcRepository(dialect = Dialect.MYSQL) (1)
public interface BookRepository extends GenericRepository<BookEntity, Long>, (2)
JpaSpecificationExecutor<BookEntity> { (3)
@NonNull
List<BookEntity> findAll(PredicateSpecification<BookEntity> spec); (4)
@NonNull
List<BookEntity> findAll();
@NonNull
BookEntity save(@NonNull @NotBlank String name,
@NonNull @NotNull BookAvailability availability,
@NonNull @NotBlank String author,
@NonNull @NotBlank String isbn);
}
1 | @JdbcRepository with a specific dialect. |
2 | BookEntity , the entity to treat as the root entity for the purposes of querying, is established either from the method signature or from the generic type parameter specified to the GenericRepository interface. |
3 | Implement the JpaSpecificationExecutor interface when you need create queries dynamically by composing JPA criteria. |
4 | To find multiple entities, you can use the findAll method from the JpaSpecificationExecutor interface. |
In the above code snippet, we extended the JpaSpecificationExecutor
interface to define a findAll
method that supports Predicate
as argument, which allows to modify the operations performed during runtime. We will now create a factory class for creating predicates that we plan to use in our application:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.repository.jpa.criteria.PredicateSpecification;
public class BookSpecifications {
public static PredicateSpecification<BookEntity> nameLike(@NonNull String name) {
return (root, criteriaBuilder) -> criteriaBuilder.like(root.get("name"), "%"+name+"%");
}
public static PredicateSpecification<BookEntity> authorLike(@NonNull String author) {
return (root, criteriaBuilder) -> criteriaBuilder.like(root.get("author"), "%"+author+"%");
}
}
8. Writing the Controller Logic
If you look inside the generated BookInfo.java
file, you can see the class that was generated with all the parameters
based on our definition. Notice that the constructor signature has two parameters, which were defined as required
in the
YAML definition file:
public BookInfo(String name, BookAvailability availability);
Along with that it has getters and setters for parameters and Jackson serialization annotations.
8.1. Implementing Controller Methods
Now open BooksController
. Thanks to the @Controller annotation, an instance of the class will be initialized when Micronaut application starts, and the corresponding method will be called when there is a request. The class should also have two methods named the same as the operations we created in the definition file. The methods have Micronaut framework annotations describing the required API. We will now write their bodies.
Using the Inversion of Control principle, we will inject BookRepository
so it can be used in the methods. When initializing the controller, Micronaut will automatically provide an instance of the repository as a constructor argument:
private final BookRepository bookRepository; (1)
public BooksController(BookRepository bookRepository) { (1)
this.bookRepository = bookRepository;
}
1 | Use constructor injection to inject a bean of type BookRepository . |
Next, keeping all the generated annotations, add this implementation for the search
method:
@ExecuteOn(TaskExecutors.IO) (1)
public List<BookInfo> search(
@QueryValue(value="book-name") @Nullable @Size(min=3) String bookName,
@QueryValue(value="author-name") @Nullable String authorName) {
return searchEntities(bookName, authorName)
.stream()
.map(this::map) (5)
.collect(Collectors.toList());
}
private BookInfo map(BookEntity entity) {
BookInfo book = new BookInfo(entity.getName(), entity.getAvailability());
book.setISBN(entity.getIsbn());
book.setAuthor(entity.getAuthor());
return book;
}
@NonNull
private List<BookEntity> searchEntities(@Nullable String name, @Nullable String author) { (2)
if (StringUtils.isEmpty(name) && StringUtils.isEmpty(author)) {
return bookRepository.findAll();
} else if (StringUtils.isEmpty(name)) {
return bookRepository.findAll(BookSpecifications.authorLike(author)); (3)
} else if (StringUtils.isEmpty(author)) {
return bookRepository.findAll(BookSpecifications.nameLike(name));
} else {
return bookRepository.findAll(BookSpecifications.authorLike(author)
.and(BookSpecifications.nameLike(name))); (4)
}
}
1 | It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop. |
2 | Define the searchEntities method that will manage the different combinations of desired search parameters. |
3 | Use the predicate we previously defined to search for substring in one column |
4 | Use the Criteria API to build a query for combined search in the 2 columns during runtime. |
5 | Map the BookEntity instances to the desired return type. |
Finally, we will implement the addBook
method:
@ExecuteOn(TaskExecutors.IO) (1)
@Status(OK) (2)
public void addBook(@Body @NotNull @Valid BookInfo bookInfo) {
bookRepository.save(bookInfo.getName(), (3)
bookInfo.getAvailability(),
bookInfo.getAuthor(),
bookInfo.getISBN());
}
1 | It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop. |
2 | You can return void in your controller’s method and specify the HTTP status code via the @Status annotation. |
3 | Call the repository method that will add an entry to the table. |
9. Test Resources
When the application is started locally — either under test or by running the application — resolution of the datasource URL is detected and the Test Resources service will start a local MySQL docker container, and inject the properties required to use this as the datasource.
For more information, see the JDBC section or R2DBC section of the Test Resources documentation.
10. Running the Application
To run the application, use the ./mvnw mn:run
command, which starts the application on port 8080.
You can send a few requests to the paths to test the application. We will use cURL for that.
-
The search for book names, that have
"Guide"
as substring should return 2BookInfo
objects:curl "localhost:8080/search?book-name=Guide"
[{"name":"The Hitchhiker's Guide to the Galaxy","availability":"reserved","author":"Douglas Adams"}, {"name":"Java Guide for Beginners","availability":"available"}]
-
The search for a substring
"Gu"
in name will return a"Bad Request"
error, since we have defined thebook-name
parameter to have at least three characters:curl -i "localhost:8080/search?book-name=Gu"
HTTP/1.1 400 Bad Request Content-Type: application/json date: **** content-length: 180 connection: keep-alive {"message":"Bad Request","_embedded":{"errors":[{"message":"bookName: size must be between 3 and 2147483647"}]}, "_links":{"self":{"href":"/search?book-name=Gu","templated":false}}}
-
Addition of a new book should not result in errors:
curl -i -d '{"name": "My book", "availability": "available"}' \ -H 'Content-Type: application/json' -X POST localhost:8080/add
HTTP/1.1 200 OK date: Tue, 1 Feb 2022 00:01:57 GMT Content-Type: application/json content-length: 0 connection: keep-alive
You can then verify that the addition was successful by performing another search.
11. Testing the Application
To run the tests:
./mvnw test
11.1. Testing Models
As we have noticed previously, some files were generated as templates for tests. We will implement tests for models inside these files. Their main purpose will be to verify that we correctly described our API in the YAML file, and therefore the generated files behave as expected.
We will begin by writing tests for the required properties of BookInfo
object.
Define the following imports:
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import javax.validation.Validator;
import static org.junit.jupiter.api.Assertions.*;
Add the following methods inside the BookInfoTest
class:
@Inject
Validator validator; (1)
@Test
public void nameTest() {
BookInfo bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE);
assertTrue(validator.validate(bookInfo).isEmpty()); (2)
bookInfo = new BookInfo(null, BookAvailability.AVAILABLE);
assertFalse(validator.validate(bookInfo).isEmpty()); (3)
}
@Test
public void availabilityTest() { (4)
BookInfo bookInfo = new BookInfo("ALice's Adventures in Wonderland", BookAvailability.RESERVED);
assertTrue(validator.validate(bookInfo).isEmpty());
bookInfo = new BookInfo("Alice's Adventures in Wonderland", null);
assertFalse(validator.validate(bookInfo).isEmpty());
}
1 | Instruct Micronaut to inject an instance of the Validator.
Validator will automatically validate parameters and response bodies annotated with @Valid in the controller.
We will use it to test the validations manually. |
2 | Verify that the validator doesn’t produce any violations on a correct BookInfo instance. |
3 | Verify that null value is not allowed for the name property, since the property is marked as required. |
4 | Perform the same tests for the required availability property. |
We will then write similar tests for other properties:
@Test
public void authorTest() {
BookInfo bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.author(null);
assertTrue(validator.validate(bookInfo).isEmpty());
bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.author("Lewis Carroll");
assertTrue(validator.validate(bookInfo).isEmpty()); (1)
bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.author("fo");
assertFalse(validator.validate(bookInfo).isEmpty()); (2)
}
@Test
public void ISBNTest() {
BookInfo bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.ISBN(null);
assertTrue(validator.validate(bookInfo).isEmpty());
bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.ISBN("9783161484100");
assertTrue(validator.validate(bookInfo).isEmpty()); (3)
bookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.ISBN("9783161 84100");
assertFalse(validator.validate(bookInfo).isEmpty()); (4)
}
1 | Verify that there are no violations for both null or "Lewis Carol" used as a value for the author property. |
2 | Verify that there is a violation if the name is too short (at least tree characters are required). |
3 | Verify that there are no violations for valid values of the ISBN property. |
4 | Verify that there is a violation if the value doesn’t match the required pattern (A space is present). |
Finally, we will test JSON serialization and parsing by writing a simple controller and client:
@Property(name = "spec.name", value = "BookInfoTest") (2)
@MicronautTest
public class BookInfoTest {
@Inject
@Client("/")
HttpClient httpClient;
@Test
public void bookInfoJsonSerialization() {
BookInfo requiredBookInfo = new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.author("Lewis Carroll")
.ISBN("9783161484100");
BookInfo bookInfo = httpClient.toBlocking().retrieve(HttpRequest.GET("/bookinfo"), BookInfo.class); (5)
assertEquals(requiredBookInfo, bookInfo);
}
@Requires(property = "spec.name", value = "BookInfoTest") (3)
@Controller("/bookinfo") (1)
static class BookInfoSerdeController {
@PermitAll
@Get
BookInfo index() { (4)
return new BookInfo("Alice's Adventures in Wonderland", BookAvailability.AVAILABLE)
.author("Lewis Carroll")
.ISBN("9783161484100");
}
}
1 | Create a simple controller that will respond to requests on the /bookinfo path. |
2 | Specify the spec.name property for this test class. |
3 | Use the Requires annotation to specify that
this controller will only be used if the spec.name property is set to BookInfoTest . This will prevent the controller
from running during other tests. |
4 | Define a GET method that will return a BookInfo object in the application/json format. |
5 | Create a test that will send a request to the server and verify that the response matches the desired object (This means that both serialization and parsing work correctly). |
Similarly, we can implement tests for the BookAvailability
class. The details are not shown in this guide.
11.2. Testing the Controller
We will write tests for the two paths of BookController
.
If you open the BooksControllerTest
, you can see that templates of tests were generated for both paths with examples of requests to corresponding paths. The templates can be used to simplify and speed up test writing.
We will simply replace the contents of the file:
package example.micronaut.controller;
import example.micronaut.model.BookAvailability;
import example.micronaut.model.BookInfo;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.core.type.Argument;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import java.util.List;
@MicronautTest (1)
public class BooksControllerTest {
@Inject
@Client("${context-path}")
HttpClient client; (2)
@Test
void addBookClientApiTest() {
BookInfo body = new BookInfo("Building Microservices", BookAvailability.AVAILABLE);
body.setAuthor("Sam Newman");
body.setISBN("9781492034025");
HttpResponse<?> response = client.toBlocking()
.exchange(HttpRequest.POST("/add", body)); (3)
assertEquals(HttpStatus.OK, response.status()); (4)
}
@Test
void searchClientApiTest() {
HttpResponse<List<BookInfo>> response = client.toBlocking()
.exchange(HttpRequest.GET(UriBuilder.of("/search")
.queryParam("book-name", "Guide")
.build()
), Argument.listOf(BookInfo.class)); (5)
List<BookInfo> body = response.body(); (6)
assertEquals(HttpStatus.OK, response.status());
assertEquals(2, body.size()); (7)
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
2 | Inject the HttpClient bean and point it to the embedded server. |
3 | Creating HTTP Requests is easy thanks to the Micronaut framework fluid API. |
4 | Verify that addition of book info was successful by checking the status code. |
5 | Micronaut HTTP Client simplifies binding a JSON array to a list of POJOs by using Argument::listOf . |
6 | Use .body() to retrieve the parsed payload. |
7 | Verify that there are exactly two books with "Guide" substring in title. |
To run the tests:
./mvnw test
All the tests should run successfully.
12. Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.
Compiling native executables ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
12.1. Native executable generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
To generate a native executable using Maven, run:
./mvnw package -Dpackaging=native-image
The native executable is created in the target
directory and can be run with target/micronautguide
.
13. Next steps
13.1. Learn More
Read OpenAPI and Micronaut documentation and guides:
-
Definition files generation from annotated controllers with Micronaut OpenAPI
13.2. Add Security
We could have defined our security requirements by adding a security schema to the library-definition.yaml
file.
For example, we will add HTTP Basic authentication:
paths:
/search:
# ... #
/add:
post:
# ... #
security:
- MyBasicAuth: [] (2)
components:
schemas:
# ... #
securitySchemes:
MyBasicAuth: (1)
type: http
scheme: basic
1 | Define a security schema inside the components/securitySchemes . We want to use Basic auth for authentication. |
2 | Add the schema to the paths that you want to secure. In this case, we want to restrict access to adding books into our library. |
You can read more about describing various authentication in the "Authentication and Authorization" OpenAPI guide. |
The generator will then annotate such endpoints with the Secured annotation accordingly:
@Secured(SecurityRule.IS_AUTHENTICATED)
public Mono<Object> addBook( /* ... */ ){ /* ... */ }
You will then need to implement an
AuthenticationProvider
that satisfies your needs. If you want to finish implementing the basic authentication, continue to the
Micronaut Basic Auth guide and replicate
steps to create the AuthenticationProvider
and appropriate tests.
You can also read Micronaut Security documentation or Micronaut guides about security to learn more about the supported Authorization strategies. |
13.3. Generate Micronaut Client
You can generate a Micronaut client based on the same library-definition.yaml
file.
Run the following in terminal to create client in the library-client
directory:
java -jar openapi-generator-cli-XXX.jar generate \
-g java-micronaut-client \
-i library-definition.yaml \
-o library-client \
-p apiPackage=example.micronaut.api \
-p modelPackage=example.micronaut.model \
-p build=maven \
-p test=junit
13.4. Add Server URL Information
If you have your server running, you can add your website URL to it in the YAML definition file:
# ... #
servers:
- url: 'http://my.website.com'
13.5. Generate User-Friendly Documentation
You can generate documentation in html file inside the html-docs/
directory by running
java -jar openapi-generator-cli-XXX.jar generate \
-g html2 \
-i library-definition.yaml \
-o html-docs