DEV Community

Cover image for 🥉 Micronaut: Top 5 Server-Side Frameworks for Kotlin in 2022
Roger Viñas Alcon
Roger Viñas Alcon

Posted on • Updated on

🥉 Micronaut: Top 5 Server-Side Frameworks for Kotlin in 2022

This is a demo inspired by @antonarhipov's Top 5 Server-Side Frameworks for Kotlin in 2022 @ Kotlin by JetBrains where, spoiler alert, the author shares this top 5 list:

🥇 Spring Boot
🥈 Quarkus
🥉 Micronaut
🏅 Ktor
🏅 http4k

I have a lot of experience in Spring Boot, so I wanted to take a look at the other ones 😜
Meme

To do so we will create a simple application with each one of these frameworks, implementing the following scenario:
Scenario

GitHub logo rogervinas / top-5-server-side-kotlin-frameworks-2022

⭐ Top 5 Server-Side Frameworks for Kotlin in 2022

This post will describe the step-by-step Micronaut implementation, you can check the other ones in this series too.

To begin with you can follow the Creating your first Micronaut application guide.

You will see that there are two alternatives:

You can also check all the other guides as well as the user documentation.

To create our sample gradle & kotlin application using the command line:

sdk install micronaut
mn create-app micronaut-app \
   --features=data-jdbc,postgres,flyway \
   --build=gradle_kotlin --lang=kotlin --java-version=17 \
   --test=junit
Enter fullscreen mode Exit fullscreen mode

Just run it once to check everything is ok:

./gradlew run
Enter fullscreen mode Exit fullscreen mode

And make a request to the health endpoint:

curl http://localhost:8080/health
{"status":"UP"}
Enter fullscreen mode Exit fullscreen mode

Implementation

YAML configuration

We can add to application.yaml our first configuration property:

greeting:
  name: "Bitelchus"
Enter fullscreen mode Exit fullscreen mode

We can have different "environments" active (similar to "profiles" in Spring Boot):

  • By default, no environment is enabled, so only application.yaml file will be loaded.
  • We can enable environments using MICRONAUT_ENVIRONMENTS environment variable or micronaut.environments system property.
  • When executing tests test environment is enabled.

So we will create a application-prod.yaml file to put there all the production configuration properties.

More documentation about configuration sources at Application Configuration guide.

GreetingRepository

We will create a GreetingRepository:

interface GreetingRepository {
  fun getGreeting(): String
}

@Singleton
open class GreetingJdbcRepository(dataSource: DataSource)
  : GreetingRepository {

  private val jdbi = Jdbi.create(dataSource)

  @Transactional
  override fun getGreeting(): String = jdbi
    .open()
    .use {
      it
        .createQuery(
          """
            SELECT greeting FROM greetings
            ORDER BY random() LIMIT 1
          """.trimIndent()
        )
        .mapTo(String::class.java)
        .first()
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The @Singleton annotation will make Micronaut to create an instance at startup.
  • We inject the DataSource provided by Micronaut.
  • As seems that Micronaut does not include anything similar by default, we use JDBI and that SQL to retrieve one random greeting from the greetings table.
  • We add the @Transactional annotation so Micronaut will execute the query within a database transaction. As Micronaut will instantiate a proxy class inheriting from GreetingJdbcRepository we are forced to "open" the class as all kotlin classes are final.

For this to work, we need some extra steps ...

Use a specific postresql driver version (just not do depend on Micronaut BOM) and add the JDBI dependency:

implementation("org.postgresql:postgresql:42.5.1")
implementation("org.jdbi:jdbi3-core:3.36.0")
Enter fullscreen mode Exit fullscreen mode

As we added posgtresql feature when creating the project, Test Resources will magically start for us a PostgreSQL container. It is not required but we can configure a specific version of the container in application.yaml:

test-resources:
  containers:
    postgres:
      image-name: "postgres:14.5"
Enter fullscreen mode Exit fullscreen mode

We should also configure the database connection for prod environment in application-prod.yaml:

datasources:
  default:
    url: "jdbc:postgresql://${DB_HOST:localhost}:5432/mydb"
    username: "myuser"
    password: "mypassword"
Enter fullscreen mode Exit fullscreen mode

Configuring it will disable Test Resources for PostgreSQL on prod environment.

Flyway is already enabled as we created the project adding the flyway feature, so we only need to add migrations under src/main/resources/db/migration to create and populate greetings table.

GreetingController

We will create a GreetingController to serve the /hello endpoint:

@Controller("/hello")
class GreetingController(
  private val repository: GreetingRepository,
  @Property(name = "greeting.name")
  private val name: String,
  @Property(name = "greeting.secret", defaultValue = "unknown")
  private val secret: String
) {
    @Get
    @Produces(MediaType.TEXT_PLAIN)
    fun hello() =
      "${repository.getGreeting()} my name is $name" +
        " and my secret is $secret"
}
Enter fullscreen mode Exit fullscreen mode
  • We can inject dependencies via constructor and configuration properties using @Property annotation.
  • We expect to get greeting.secret from Vault, that is why we configure unknown as its default value, so it does not fail until we configure Vault properly.
  • Everything is pretty similar to Spring Boot.

Vault configuration

Following the HashiCorp Vault Support guide we have add this configuration to bootstrap.yaml:

micronaut:
  application:
    name: "myapp"
  server:
    port: 8080
  config-client:
    enabled: true

vault:
  client:
    config:
      enabled: true
    kv-version: "V2"
    secret-engine-name: "secret"

test-resources:
  containers:
    postgres:
      image-name: "postgres:14.5"
    hashicorp-vault:
      image-name: "vault:1.12.1"
      path: "secret/myapp"
      secrets:
        - "greeting.secret=watermelon"
Enter fullscreen mode Exit fullscreen mode

Note that some of this configuration was already set in application.yaml but we move it here, so it is available in the "bootstrap" phase.

Once thing not currently mentioned in the documentation is that we need to add this dependency to enable the "bootstrap" phase:

implementation("io.micronaut.discovery:micronaut-discovery-client")
Enter fullscreen mode Exit fullscreen mode

Test Resources for Hashicorp Vault allow us to populate Vault, so for dev and test it will start a ready-to-use Vault container 🥹

For prod environment we configure Vault in bootstrap-prod.yaml:

vault:
  client:
    uri: "http://${VAULT_HOST:localhost}:8200"
    token: "mytoken"
Enter fullscreen mode Exit fullscreen mode

And as usual, configuring it will disable Test Resources for Vault on prod environment.

Testing the endpoint

We can test the endpoint this way:

@MicronautTest
@Property(name = "greeting.secret", value = "apple")
class GreetingControllerTest {

  @Inject
  @field:Client("/")
  private lateinit var client: HttpClient

  @Inject
  private lateinit var repository: GreetingRepository

  @Test
  fun `should say hello`() {
    every { repository.getGreeting() } returns "Hello"

    val request: HttpRequest<Any> = HttpRequest.GET("/hello")
    val response = client.toBlocking()
      .exchange(request, String::class.java)

    assertEquals(OK, response.status)
    assertEquals(
      "Hello my name is Bitelchus and my secret is apple", 
      response.body.get()
    )
  }

    @MockBean(GreetingRepository::class)
    fun repository() = mockk<GreetingRepository>()
}
Enter fullscreen mode Exit fullscreen mode
  • @MicronautTest will start all "Test Resources" containers, despite not needed 🤷
  • We mock the repository with @MockBean.
  • We use Micronaut's HttpClient to test the endpoint.
  • We set greeting.secret property just for this test (so we do not use the Vault value).

Testing the application

We can test the whole application this way:

@MicronautTest
class GreetingApplicationTest {

  @Test
  fun `should say hello`(spec: RequestSpecification) {
    spec
      .`when`()
      .get("/hello")
      .then()
      .statusCode(200)
      .body(
        matchesPattern(
          ".+ my name is Bitelchus and my secret is watermelon"
        )
      )
    }
}
Enter fullscreen mode Exit fullscreen mode
  • @MicronautTest will start all "Test Resources" containers, now all of them are being used.
  • For this one we use Rest Assured instead of HttpClient, just to show another way. We need to add io.micronaut.test:micronaut-test-rest-assured dependency.
  • We use pattern matching to check the greeting, as it is random.
  • As this test uses Vault, the secret should be watermelon.

Test

./gradlew test
Enter fullscreen mode Exit fullscreen mode

Run

# Start Application
./gradlew run

# Make requests
curl http://localhost:8080/hello
Enter fullscreen mode Exit fullscreen mode

Build a fatjar and run it

# Build fatjar
./gradlew shadowJar

# Start Vault and Database
docker compose up -d vault vault-cli db

# Start Application
java -Dmicronaut.environments=prod \
  -jar build/libs/micronaut-app-0.1-all.jar

# Make requests
curl http://localhost:8080/hello

# Stop Application with control-c

# Stop all containers
docker compose down
Enter fullscreen mode Exit fullscreen mode

Build a docker image and run it

Micronaut configures a base docker image by default but we can customize it in build.gradle.kts:

tasks.named<MicronautDockerfile>("dockerfile") {
  baseImage.set("eclipse-temurin:17-jre-alpine")
}
Enter fullscreen mode Exit fullscreen mode

Then:

# Build docker image
./gradlew dockerBuild

# Start Vault and Database
docker compose up -d vault vault-cli db

# Start Application
docker compose --profile micronaut up -d

# Make requests
curl http://localhost:8080/hello

# Stop all containers
docker compose --profile micronaut down
docker compose down
Enter fullscreen mode Exit fullscreen mode

Build a native executable and run it

Following Generate a Micronaut Application Native Executable with GraalVM:

# Install GraalVM via sdkman
sdk install java 22.3.r19-grl
sdk default java 22.3.r19-grl
export GRAALVM_HOME=$JAVA_HOME

# Install the native-image
gu install native-image

# Build native executable
./gradlew nativeCompile

# Start Vault and Database
docker compose up -d vault vault-cli db

# Start Application using native executable
MICRONAUT_ENVIRONMENTS=prod \
  ./build/native/nativeCompile/micronaut-app 

# Make requests
curl http://localhost:8080/hello

# Stop Application with control-c

# Stop all containers
docker compose down
Enter fullscreen mode Exit fullscreen mode

💡 As there were some reflection problems I had to add this reflect-config.json configuration generated with the native agent.

That's it! Happy coding! 💙

Top comments (0)