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 😜
To do so we will create a simple application with each one of these frameworks, implementing the following scenario:
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:
- Use Micronaut Command Line Interface command line (easily installable via sdkman)
- Use Micronaut Launch web interface (similar to Spring Initializr for Spring Boot)
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
Just run it once to check everything is ok:
./gradlew run
And make a request to the health endpoint:
curl http://localhost:8080/health
{"status":"UP"}
Implementation
YAML configuration
We can add to application.yaml
our first configuration property:
greeting:
name: "Bitelchus"
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 ormicronaut.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()
}
}
- 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 thegreetings
table. - We add the
@Transactional
annotation so Micronaut will execute the query within a database transaction. As Micronaut will instantiate a proxy class inheriting fromGreetingJdbcRepository
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")
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"
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"
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"
}
- We can inject dependencies via constructor and configuration properties using
@Property
annotation. - We expect to get
greeting.secret
from Vault, that is why we configureunknown
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"
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")
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"
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>()
}
-
@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"
)
)
}
}
-
@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 addio.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
Run
# Start Application
./gradlew run
# Make requests
curl http://localhost:8080/hello
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
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")
}
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
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
💡 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)