Access a database with MyBatis

Learn how to access a database with MyBatis using the Micronaut framework.

Authors: Iván López, Sergio del Amo

Micronaut Version: 3.7.0

1. Getting Started

Learn how to access a database with MyBatis using the Micronaut framework.

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. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide --build=gradle --lang=kotlin
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

4.1. Enable annotation Processing

If you use Java or Kotlin and IntelliJ IDEA, make sure to enable annotation processing.

annotationprocessorsintellij

4.2. Configure Data Source and JPA

Add the following snippet to include the necessary dependencies:

build.gradle
implementation("org.mybatis:mybatis:3.5.10") (1)
implementation("io.micronaut.sql:micronaut-jdbc-hikari") (2)
runtimeOnly("com.h2database:h2") (3)
1 Add MyBatis dependency.
2 Configures SQL DataSource instances using Hikari Connection Pool.
3 Add dependency to in-memory H2 Database.

Define the data source in src/main/resources/application.yml.

src/main/resources/application.yml
datasources:
  default:
    url: jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password: ""
    driverClassName: org.h2.Driver

4.3. MyBatis configuration

As there is no out-of-the-box support yet in the Micronaut framework for MyBatis, it is necessary to manually wire SqlSessionFactory.

Create the following @Factory class:

src/main/kotlin/example/micronaut/MybatisFactory.kt
package example.micronaut

import io.micronaut.context.annotation.Factory
import jakarta.inject.Singleton
import org.apache.ibatis.mapping.Environment
import org.apache.ibatis.session.Configuration
import org.apache.ibatis.session.SqlSessionFactory
import org.apache.ibatis.session.SqlSessionFactoryBuilder
import org.apache.ibatis.transaction.TransactionFactory
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory
import javax.sql.DataSource

@Factory (1)
class MybatisFactory(private val dataSource: DataSource) { (2)

    @Singleton (3)
    fun sqlSessionFactory(): SqlSessionFactory {
        val transactionFactory: TransactionFactory = JdbcTransactionFactory()
        val environment = Environment("dev", transactionFactory, dataSource) (4)
        val configuration = Configuration(environment)
        configuration.addMappers("example.micronaut") (5)
        return SqlSessionFactoryBuilder().build(configuration) (6)
    }
}
1 Annotate the class with @Factory.
2 Use constructor injection to inject a bean of type DataSource.
3 Define a @Bean of type SqlSessionFactory.
4 Use the dataSource to create a new MyBatis environment.
5 Define the package to scan for mappers.
6 Create a new SqlSessionFactory bean.

4.4. Domain

Create the domain entities:

src/main/kotlin/example/micronaut/domain/Genre.kt
package example.micronaut.domain

import com.fasterxml.jackson.annotation.JsonIgnore
import io.micronaut.core.annotation.Introspected

@Introspected
data class Genre(var name: String) {

    var id: Long? = null

    @JsonIgnore
    var books: Set<Book> = mutableSetOf()

    override fun toString() = "Genre{id=$id, name='$name', books=$books}"
}
src/main/kotlin/example/micronaut/domain/Book.kt
package example.micronaut.domain

import io.micronaut.core.annotation.Introspected

@Introspected
data class Book(
    var isbn: String,
    var name: String,
    var genre: Genre?) {

    var id: Long? = null

    override fun toString() = "Book{id=$id, name='$name', isbn='$isbn', genre=$genre}"
}

4.5. Repository Access

Create an interface to define the operations to access the database and use MyBatis annotations to map the methods to SQL queries:

src/main/kotlin/example/micronaut/genre/GenreMapper.kt
package example.micronaut.genre

import example.micronaut.domain.Genre
import org.apache.ibatis.annotations.Delete
import org.apache.ibatis.annotations.Insert
import org.apache.ibatis.annotations.Options
import org.apache.ibatis.annotations.Param
import org.apache.ibatis.annotations.Select
import org.apache.ibatis.annotations.Update
import javax.validation.constraints.Pattern
import javax.validation.constraints.Positive
import javax.validation.constraints.PositiveOrZero

interface GenreMapper {

    @Select("select * from genre where id=#{id}")
    fun findById(id: Long): Genre?

    @Insert("insert into genre(name) values(#{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    fun save(genre: Genre)

    @Delete("delete from genre where id=#{id}")
    fun deleteById(id: Long)

    @Update("update genre set name=#{name} where id=#{id}")
    fun update(@Param("id") id: Long, @Param("name") name: String?)

    @Select("select * from genre")
    fun findAll(): List<Genre>

    @Select("select * from genre order by \${sort} \${order}")
    fun findAllBySortAndOrder(@Param("sort") @Pattern(regexp = "id|name") sort: String,
                              @Param("order") @Pattern(regexp = "asc|ASC|desc|DESC") order: String): List<Genre>

    @Select("select * from genre order by \${sort} \${order} limit \${offset}, \${max}")
    fun findAllByOffsetAndMaxAndSortAndOrder(@Param("offset") @PositiveOrZero offset: Int,
                                             @Param("max") @Positive max: Int,
                                             @Param("sort") @Pattern(regexp = "id|name") sort: String,
                                             @Param("order") @Pattern(regexp = "asc|ASC|desc|DESC") order: String): List<Genre>

    @Select("select * from genre limit \${offset}, \${max}")
    fun findAllByOffsetAndMax(@Param("offset") @PositiveOrZero offset: Int,
                              @Param("max") @Positive max: Int): List<Genre>
}

And the implementation:

src/main/kotlin/example/micronaut/genre/GenreMapperImpl.kt
package example.micronaut.genre

import example.micronaut.domain.Genre
import jakarta.inject.Singleton
import org.apache.ibatis.session.SqlSession
import org.apache.ibatis.session.SqlSessionFactory
import javax.validation.constraints.Pattern
import javax.validation.constraints.Positive
import javax.validation.constraints.PositiveOrZero

@Singleton (1)
open class GenreMapperImpl(private val sqlSessionFactory: SqlSessionFactory) : GenreMapper { (2)

    override fun findById(id: Long): Genre? {
        sqlSessionFactory.openSession().use { sqlSession ->  (3)
            return getGenreMapper(sqlSession).findById(id) (5)
        }
    }

    override fun save(genre: Genre) {
        sqlSessionFactory.openSession().use { sqlSession ->
            getGenreMapper(sqlSession).save(genre)
            sqlSession.commit() (6)
        }
    }

    override fun deleteById(id: Long) {
        sqlSessionFactory.openSession().use { sqlSession ->
            getGenreMapper(sqlSession).deleteById(id)
            sqlSession.commit()
        }
    }

    override fun update(id: Long, name: String?) {
        sqlSessionFactory.openSession().use { sqlSession ->
            getGenreMapper(sqlSession).update(id, name)
            sqlSession.commit()
        }
    }

    override fun findAll(): List<Genre> {
        sqlSessionFactory.openSession().use { sqlSession -> return getGenreMapper(sqlSession).findAll() }
    }

    override fun findAllBySortAndOrder(@Pattern(regexp = "id|name") sort: String,
                                       @Pattern(regexp = "asc|ASC|desc|DESC") order: String): List<Genre> {
        sqlSessionFactory.openSession().use { sqlSession ->
            return getGenreMapper(sqlSession).findAllBySortAndOrder(sort, order)
        }
    }

    override fun findAllByOffsetAndMaxAndSortAndOrder(@PositiveOrZero offset: Int,
                                                      @Positive max: Int,
                                                      @Pattern(regexp = "id|name") sort: String,
                                                      @Pattern(regexp = "asc|ASC|desc|DESC") order: String): List<Genre> {
        sqlSessionFactory.openSession().use { sqlSession ->
            return getGenreMapper(sqlSession).findAllByOffsetAndMaxAndSortAndOrder(offset, max, sort, order)
        }
    }

    override fun findAllByOffsetAndMax(@PositiveOrZero offset: Int,
                                       @Positive max: Int): List<Genre> {
        sqlSessionFactory.openSession().use { sqlSession ->
            return getGenreMapper(sqlSession).findAllByOffsetAndMax(offset, max)
        }
    }

    private fun getGenreMapper(sqlSession: SqlSession): GenreMapper {
        return sqlSession.getMapper(GenreMapper::class.java) (4)
    }
}
1 Use jakarta.inject.Singleton to designate a class as a singleton.
2 Easily inject the SqlSessionFactory bean created by the @Factory.
3 Use try-with-resources to automatically close the SQL session.
4 Get MyBatis mapper implementation for the interface.
5 Execute the desired method using the mapper. This will trigger the SQL query.
6 In a database write access, commit the transaction.

Create an interface to define the high level operations exposed to the application:

src/main/kotlin/example/micronaut/genre/GenreRepository.kt
package example.micronaut.genre

import example.micronaut.ListingArguments
import example.micronaut.domain.Genre
import java.util.Optional
import javax.validation.constraints.NotBlank

interface GenreRepository {

    fun findById(id: Long): Optional<Genre>

    fun save(@NotBlank name: String): Genre

    fun deleteById(id: Long)

    fun findAll(args: ListingArguments): List<Genre>

    fun update(id: Long, @NotBlank name: String): Int
}

And the implementation using GenreMapper:

src/main/kotlin/example/micronaut/genre/GenreRepositoryImpl.kt
package example.micronaut.genre

import example.micronaut.ListingArguments
import example.micronaut.domain.Genre
import io.micronaut.core.annotation.NonNull
import jakarta.inject.Singleton
import java.util.Optional
import javax.validation.constraints.NotBlank

@Singleton (1)
open class GenreRepositoryImpl(private val genreMapper: GenreMapper) : GenreRepository {

    override fun findById(id: Long): Optional<Genre> =
        Optional.ofNullable(genreMapper.findById(id))

    @NonNull
    override fun save(@NotBlank name: String): Genre {
        val genre = Genre(name)
        genreMapper.save(genre)
        return genre
    }

    override fun deleteById(id: Long) {
        findById(id).ifPresent { genreMapper.deleteById(id) }
    }

    @NonNull
    override fun findAll(args: ListingArguments): List<Genre> {
        if (args.getMax().isPresent && args.getSort().isPresent && args.getOffset().isPresent && args.getOrder().isPresent) {
            return genreMapper.findAllByOffsetAndMaxAndSortAndOrder(
                    args.getOffset().get(),
                    args.getMax().get(),
                    args.getSort().get(),
                    args.getOrder().get())
        }

        if (args.getMax().isPresent && args.getOffset().isPresent && (!args.getSort().isPresent || !args.getOrder().isPresent)) {
            return genreMapper.findAllByOffsetAndMax(args.getOffset().get(), args.getMax().get())
        }

        if ((!args.getMax().isPresent || !args.getOffset().isPresent) && args.getSort().isPresent && args.getOrder().isPresent) {
            return genreMapper.findAllBySortAndOrder(args.getSort().get(), args.getOrder().get())
        }

        return genreMapper.findAll()
    }

    override fun update(id: Long, @NotBlank name: String): Int {
        genreMapper.update(id, name)
        return -1
    }
}

4.6. Controller

Micronaut validation is built on the standard framework – JSR 380, also known as Bean Validation 2.0.

Hibernate Validator is a reference implementation of the validation API. Micronaut has built-in support for validation of beans that are annotated with javax.validation annotations.

The necessary dependencies are included by default when creating a new application, so you don’t need to add anything else.

Create two classes to encapsulate Save and Update operations:

src/main/kotlin/example/micronaut/genre/GenreSaveCommand.kt
package example.micronaut.genre

import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.NotBlank

@Introspected
data class GenreSaveCommand(@NotBlank var name: String)
src/main/kotlin/example/micronaut/genre/GenreUpdateCommand.kt
package example.micronaut.genre

import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.NotBlank

@Introspected
class GenreUpdateCommand(var id: Long, @NotBlank var name: String)

Create a POJO to encapsulate Sorting and Pagination:

src/main/kotlin/example/micronaut/ListingArguments.kt
package example.micronaut

import io.micronaut.core.annotation.Introspected
import io.micronaut.http.uri.UriBuilder
import java.net.URI
import java.util.Optional
import javax.validation.constraints.Pattern
import javax.validation.constraints.Positive
import javax.validation.constraints.PositiveOrZero

@Introspected
class ListingArguments {

    @PositiveOrZero
    private var offset: Int? = 0

    @Positive
    private var max: Int? = null

    @Pattern(regexp = "id|name")
    private var sort: String? = null

    @Pattern(regexp = "asc|ASC|desc|DESC")
    private var order: String? = null

    fun getOffset(): Optional<Int> = Optional.ofNullable(offset)

    fun setOffset(offset: Int?) {
        this.offset = offset
    }

    fun getMax(): Optional<Int> = Optional.ofNullable(max)

    fun setMax(max: Int?) {
        this.max = max
    }

    fun getSort(): Optional<String> = Optional.ofNullable(sort)

    fun setSort(sort: String?) {
        this.sort = sort
    }

    fun getOrder(): Optional<String> = Optional.ofNullable(order)

    fun setOrder(order: String?) {
        this.order = order
    }

    fun of(uriBuilder: UriBuilder): URI {
        if (max != null) {
            uriBuilder.queryParam("max", max)
        }
        if (order != null) {
            uriBuilder.queryParam("order", order)
        }
        if (offset != null) {
            uriBuilder.queryParam("offset", offset)
        }
        if (sort != null) {
            uriBuilder.queryParam("sort", sort)
        }
        return uriBuilder.build()
    }

    class Builder {

        private val args = ListingArguments()

        fun max(max: Int): Builder {
            args.setMax(max)
            return this
        }

        fun sort(sort: String?): Builder {
            args.setSort(sort)
            return this
        }

        fun order(order: String?): Builder {
            args.setOrder(order)
            return this
        }

        fun offset(offset: Int): Builder {
            args.setOffset(offset)
            return this
        }

        fun build(): ListingArguments = args
    }

    companion object {
        fun builder(): Builder = Builder()
    }
}

Create a ConfigurationProperties class to encapsulate the configuration of the default max value.

src/main/kotlin/example/micronaut/ApplicationConfiguration.kt
package example.micronaut

interface ApplicationConfiguration {
    val max: Int
}
src/main/kotlin/example/micronaut/ApplicationConfigurationProperties.kt
package example.micronaut

import io.micronaut.context.annotation.ConfigurationProperties

@ConfigurationProperties("application") (1)
class ApplicationConfigurationProperties : ApplicationConfiguration {

    private val DEFAULT_MAX = 10

    override var max = DEFAULT_MAX
}

Create GenreController, a controller which exposes a resource with the common CRUD operations:

src/main/kotlin/example/micronaut/GenreController.kt
package example.micronaut

import example.micronaut.domain.Genre
import example.micronaut.genre.GenreRepository
import example.micronaut.genre.GenreSaveCommand
import example.micronaut.genre.GenreUpdateCommand
import io.micronaut.http.HttpHeaders.LOCATION
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpHeaders
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 java.net.URI
import javax.validation.Valid

@Controller("/genres") (1)
open class GenreController(private val genreRepository: GenreRepository) { (2)

    @Get("/{id}") (3)
    fun show(id: Long): Genre = genreRepository.findById(id).orElse(null) (4)

    @Put (5)
    open fun update(@Body @Valid command: GenreUpdateCommand): HttpResponse<*> { (6)
        genreRepository.update(command.id, command.name)
        return HttpResponse
                .noContent<Any>()
                .header(LOCATION, location(command.id).path) (7)
    }

    @Get(value = "/list{?args*}") (8)
    open fun list(@Valid args: ListingArguments): List<Genre> = genreRepository.findAll(args)

    @Post (9)
    open fun save(@Body @Valid cmd: GenreSaveCommand): HttpResponse<Genre> {
        val genre = genreRepository.save(cmd.name)
        return HttpResponse
                .created(genre)
                .headers { headers: MutableHttpHeaders -> headers.location(location(genre.id)) }
    }

    @Delete("/{id}") (10)
    fun delete(id: Long): HttpResponse<*> {
        genreRepository.deleteById(id)
        return HttpResponse.noContent<Any>()
    }

    private fun location(id: Long?): URI = URI.create("/genres/$id")
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /genres.
2 Constructor injection.
3 Maps a GET request to /genres/{id} which attempts to show a genre. This illustrates the use of a URL path variable.
4 Returning null when the genre doesn’t exist makes the Micronaut framework respond with 404 (not found).
5 Maps a PUT request to /genres which attempts to update a genre.
6 Add @Valid to any method parameter which requires validation. Use a POJO supplied as a JSON payload in the request to populate command.
7 It is easy to add custom headers to the response.
8 Maps a GET request to /genres which returns a list of genres. This mapping illustrates optional URL parameters.
9 Maps a POST request to /genres which attempts to save a genre.
10 Maps a DELETE request to /genres/{id} which attempts to remove a genre. This illustrates the use of a URL path variable.

4.7. 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:

build.gradle
implementation("io.micronaut.flyway:micronaut-flyway")
runtimeOnly("org.flywaydb:flyway-mysql")

We will enable Flyway in application.yml and configure it to perform migrations on one of the defined data sources.

src/main/resources/application.yml
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:

src/main/resources/db/migration/V1__schema.sql
DROP TABLE IF EXISTS GENRE;
DROP TABLE IF EXISTS BOOK;

CREATE TABLE GENRE (
  id    BIGINT SERIAL PRIMARY KEY NOT NULL,
  name VARCHAR(255)              NOT NULL UNIQUE
);

CREATE TABLE BOOK (
  id    BIGINT SERIAL PRIMARY KEY NOT NULL,
  name VARCHAR(255)              NOT NULL,
  isbn VARCHAR(255)              NOT NULL,
  genre_id BIGINT,
    constraint FKM1T3YVW5I7OLWDF32CWUUL7TA
    foreign key (GENRE_ID) references GENRE
);

During application startup, Flyway will execute the SQL file and create the schema needed for the application.

4.8. Tests

Create a JUnit test to verify the CRUD operations:

src/test/kotlin/example/micronaut/GenreControllerTest.kt
package example.micronaut

import example.micronaut.domain.Genre
import example.micronaut.genre.GenreSaveCommand
import example.micronaut.genre.GenreUpdateCommand
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.CREATED
import io.micronaut.http.HttpStatus.NO_CONTENT
import io.micronaut.http.client.BlockingHttpClient
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.http.uri.UriBuilder
import io.micronaut.http.uri.UriTemplate
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import java.util.Collections

@MicronautTest (1)
class GenreControllerTest {

    @Inject
    @field:Client("/")
    lateinit var httpClient: HttpClient (2)

    @Test
    fun supplyAnInvalidOrderTriggersValidationFailure() {
        assertThrows(HttpClientResponseException::class.java) {
            client.retrieve(
                    HttpRequest.GET<Any>("/genres/list?order=foo"),
                    Argument.of(List::class.java, Genre::class.java))
        }
    }

    @Test
    fun testFindNonExistingGenreReturns404() {
        assertThrows(HttpClientResponseException::class.java) {
            client.retrieve(HttpRequest.GET<Any>("/genres/99"), Argument.of(Genre::class.java))
        }
    }

    @Test
    fun testGenreCrudOperations() {
        val genreIds = mutableListOf<Long>()

        var response = saveGenre("DevOps")
        genreIds.add(entityId(response)!!)
        assertEquals(CREATED, response.status)

        response = saveGenre("Microservices") (3)
        assertEquals(CREATED, response.status)

        val id = entityId(response)
        genreIds.add(id!!)
        var genre = show(id)
        assertEquals("Microservices", genre.name)

        response = update(id, "Micro-services")
        assertEquals(NO_CONTENT, response.status)

        genre = show(id)
        assertEquals("Micro-services", genre.name)

        var genres = listGenres(ListingArguments.builder().build())
        assertEquals(2, genres.size)

        genres = listGenres(ListingArguments.builder().max(1).build())
        assertEquals(1, genres.size)
        assertEquals("DevOps", genres[0].name)

        genres = listGenres(ListingArguments.builder().max(1).order("desc").sort("name").build())
        assertEquals(1, genres.size)
        assertEquals("Micro-services", genres[0].name)

        genres = listGenres(ListingArguments.builder().max(1).offset(10).build())
        assertEquals(0, genres.size)

        // cleanup:
        for (genreId in genreIds) {
            response = delete(genreId)
            assertEquals(NO_CONTENT, response.status)
        }
    }

    private fun listGenres(args: ListingArguments): List<Genre> {
        val uri = args.of(UriBuilder.of("/genres/list"))
        val request: HttpRequest<*> = HttpRequest.GET<Any>(uri)
        return client.retrieve(request, Argument.of(List::class.java, Genre::class.java)) as List<Genre> (4)
    }

    private fun show(id: Long?): Genre {
        val uri = UriTemplate.of("/genres/{id}").expand(Collections.singletonMap<String, Any?>("id", id))
        val request: HttpRequest<*> = HttpRequest.GET<Any>(uri)
        return client.retrieve(request, Genre::class.java)
    }

    private fun update(id: Long?, name: String): HttpResponse<Any> {
        val request: HttpRequest<*> = HttpRequest.PUT("/genres", GenreUpdateCommand(id!!, name))
        return client.exchange(request) (5)
    }

    private fun delete(id: Long): HttpResponse<Any> {
        val request: HttpRequest<*> = HttpRequest.DELETE<Any>("/genres/$id")
        return client.exchange(request)
    }

    private fun entityId(response: HttpResponse<Any>): Long? {
        val value = response.header(HttpHeaders.LOCATION) ?: return null
        val path = "/genres/"
        val index = value.indexOf(path)
        return if (index != -1) {
            java.lang.Long.valueOf(value.substring(index + path.length))
        } else null
    }

    private val client: BlockingHttpClient
        get() = httpClient.toBlocking()

    private fun saveGenre(genre: String): HttpResponse<Any> {
        val request: HttpRequest<*> = HttpRequest.POST("/genres", GenreSaveCommand(genre)) (3)
        return client.exchange(request)
    }
}
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 If you care just about the object in the response use retrieve.
5 Sometimes, receiving just the object is not enough and you need information about the response. In this case, instead of retrieve you should use the exchange method.

Run the tests:

./gradlew test

4.9. Running the App

5. Running the Application

To run the application, use the ./gradlew run command, which starts the application on port 8080.

We can use curl to check that everything works as expected:

curl http://localhost:8080/genres/list
[]
curl -X POST -d '{"name":"Sci-fi"}' -H "Content-Type: application/json" http://localhost:8080/genres
{"id":1,"name":"Sci-fi"}
curl -X POST -d '{"name":"Science"}' -H "Content-Type: application/json" http://localhost:8080/genres
{"id":2,"name":"Science"}
curl http://localhost:8080/genres/list
[{"id":1,"name":"Sci-fi"},{"id":2,"name":"Science"}]
curl -X DELETE http://localhost:8080/genres/1
curl http://localhost:8080/genres/list
[{"id":2,"name":"Science"}]

6. 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.

6.1. Native executable generation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 11
sdk install java 22.1.0.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM.
Java 17
sdk install java 22.1.0.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 Gradle, run:

./gradlew nativeCompile

The native executable is created in build/native/nativeCompile directory and can be run with build/native/nativeCompile/micronautguide.

It is possible to customize the name of the native executable or pass additional parameters to GraalVM:

build.gradle
graalvmNative {
    binaries {
        main {
            imageName.set('mn-graalvm-application') (1)
            buildArgs.add('--verbose') (2)
        }
    }
}
1 The native executable name will now be mn-graalvm-application
2 It is possible to pass extra arguments to build the native executable

You can execute the same curl request as before to check that the native executable works.

6.2. Next Steps

Read more about Configurations for Data Access section and Flyway support in the Micronaut framework documentation.

7. Help with the Micronaut Framework

The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.