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 Http4k implementation, you can check the other ones in this series too.
To begin with you can follow the Quickstart and How-to guides.
To create a Http4k project we have three alternatives:
- Use IntelliJ Idea plugin
- Use Project Wizard web interface (similar to Spring Initializr for Spring Boot)
- Use Toolbox CLI
For example this project has been created using Project Wizard and these options:
- What kind of app are you writing? Server
- Do you need server-side WebSockets or SSE? No
- Select a server engine: Undertow (http4k team's default choice)
- Select HTTP client library: OkHttp (http4k team's go-to HTTP Client)
- Select JSON serialisation library: Jackson (http4k team's pick for JSON library, although we will not need it for this sample)
- Select a templating library: None
- Select any other messaging formats used by the app: None (not needed for this sample but good to know)
- Select any integrations that catch your eye! None (not needed for this sample but good to know)
- Select any testing technologies to battle harden the app: None (not needed for this sample but good to know)
- Application identity:
- Main class name: GreetingApplication
- Base package name: org.rogervinas
- Select a build tool: Gradle
- Select a packaging type: ShadowJar
Note some features missing with Project Wizard:
- It generates Gradle Groovy (we cannot choose Gradle Kotlin DSL 😭)
- It only configures Java 11
- It does not generate a .gitignore file
Once created you can run it to check everything is ok:
./gradlew run
And make a request to the sample endpoints:
curl http://localhost:9000/ping
pong
curl http://localhost:9000/formats/json/jackson
{"subject":"Barry","message":"Hello there!"}
Implementation
YAML configuration
Http4k does not support loading configuration values from YAML 😨
For this sample we will just enable Cloud Native Configuration and get configuration values from environment variables using the Environment
object.
GreetingRepository
We will create a GreetingRepository
:
interface GreetingRepository {
fun getGreeting(): String
}
class GreetingJdbcRepository(private val connection: Connection) : GreetingRepository {
init {
createGreetingsTable()
}
override fun getGreeting(): String = connection
.createStatement()
.use { statement ->
statement
.executeQuery("""
SELECT greeting FROM greetings
ORDER BY random() LIMIT 1
""".trimIndent()
)
.use { resultSet ->
return if (resultSet.next()) {
resultSet.getString("greeting")
} else {
throw Exception("No greetings found!")
}
}
}
private fun createGreetingsTable() {
connection.createStatement().use {
it.executeUpdate("""
CREATE TABLE IF NOT EXISTS greetings (
id serial,
greeting varchar(100) NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO greetings (greeting) VALUES ('Hello');
INSERT INTO greetings (greeting) VALUES ('Hola');
INSERT INTO greetings (greeting) VALUES ('Hi');
INSERT INTO greetings (greeting) VALUES ('Holi');
INSERT INTO greetings (greeting) VALUES ('Bonjour');
INSERT INTO greetings (greeting) VALUES ('Ni hao');
INSERT INTO greetings (greeting) VALUES ('Bon dia');
""".trimIndent()
)
}
}
}
- As Http4k does not offer any specific database support:
- We just use plain
java.sql
code (instead of any database connection library) - We just create the
greetings
table if it does not exist (instead of any database migration library like flyway)
- We just use plain
- As we plan to use Postgres we need to add
org.postgresql:postgresql
dependency inbuild.gradle
.
GreetingController
We create a GreetingController
to serve the /hello
endpoint:
fun greetingController(
name: String,
secret: String,
repository: GreetingRepository
) = routes(
"/hello" bind GET to {
Response(Status.OK)
.body("${repository.getGreeting()} my name is $name and my secret is $secret")
}
)
We just name it GreetingController
to follow the same convention as the other frameworks in this series, mainly SpringBoot.
Vault configuration
Http4k does not support Vault, so we will simply use BetterCloud/vault-java-driver:
private fun Environment.withVault(): Environment {
val vaultProtocol = EnvironmentKey.string().defaulted(VAULT_PROTOCOL, "http")(this)
val vaultHost = EnvironmentKey.string().defaulted(VAULT_HOST, "localhost")(this)
val vaultPort = EnvironmentKey.int().defaulted(VAULT_PORT, 8200)(this)
val vaultToken = EnvironmentKey.string().defaulted(VAULT_TOKEN, "mytoken")(this)
val vaultPath = EnvironmentKey.string().defaulted(VAULT_PATH, "secret/myapp")(this)
val vaultConfig = VaultConfig()
.address("$vaultProtocol://$vaultHost:$vaultPort")
.token(vaultToken)
.build()
val vaultData = Vault(vaultConfig).logical().read(vaultPath).data
return MapEnvironment.from(vaultData.toProperties()).overrides(this)
}
We use Environment
from Cloud Native Configuration to retrieve environment variables and override them with all values loaded from Vault.
Next section will show how to use this Environment.withVault()
extension.
Application
There is no particular convention in Http4k so we will make our own:
We will create a GreetingRepository
:
private fun greetingRepository(env: Environment): GreetingRepository {
val host = EnvironmentKey.string().defaulted(DB_HOST, "localhost")(env)
val port = EnvironmentKey.int().defaulted(DB_PORT, 5432)(env)
val name = EnvironmentKey.string().defaulted(DB_NAME, "mydb")(env)
val username = EnvironmentKey.string().defaulted(DB_USERNAME, "myuser")(env)
val password = EnvironmentKey.string().defaulted(DB_PASSWORD, "mypassword")(env)
val connection = DriverManager.getConnection("jdbc:postgresql://$host:$port/$name", username, password)
return GreetingJdbcRepository(connection)
}
And a HttpHandler
(our GreetingController
):
private fun greetingController(env: Environment, repository: GreetingRepository): HttpHandler {
val name = EnvironmentKey.string().defaulted(GREETING_NAME, "Bitelchus")(env)
val secret = EnvironmentKey.string().defaulted(GREETING_SECRET, "unknown")(env)
return greetingController(name, secret, repository)
}
And a Http4kServer
(our GreetingApplication
):
fun greetingApplication(env: Environment): Http4kServer {
val envWithVault = env.withVault()
val port = EnvironmentKey.int().defaulted(SERVER_PORT, 8080)(envWithVault)
val repository = greetingRepository(envWithVault)
val controller = greetingController(envWithVault, repository)
return PrintRequest().then(controller).asServer(Undertow(port))
}
And finally we will start it in the main
method:
fun main() {
println("Application starting ...")
val application = greetingApplication(ENV)
application.start()
println("Application started on " + application.port())
}
Note that in greetingApplication
function we override the ENV
object with all the values loaded from Vault.
Testing the endpoint
We can test the endpoint this way:
class GreetingControllerTest {
private val repository = mockk<GreetingRepository>().apply {
every { getGreeting() } returns "Hello"
}
private val controller = greetingController("Bitelchus", "apple", repository)
@Test
fun `should say hello`() {
val response = controller(Request(GET, "/hello"))
assertThat(
response,
hasStatus(Status.OK)
)
assertThat(
response,
hasBody("Hello my name is Bitelchus and my secret is apple")
)
}
}
- We mock the repository with
mockk
. - More documentation at TDDing http4k and Different test-types guides.
Testing the application
We can test the whole application this way:
@Testcontainers
class GreetingApplicationTest {
companion object {
@Container
private val container = DockerComposeContainer(File("../docker-compose.yaml"))
.withServices("db", "vault", "vault-cli")
.withLocalCompose(true)
.waitingFor("db", forLogMessage(".*database system is ready to accept connections.*", 1))
.waitingFor("vault", forLogMessage(".*Development mode.*", 1))
.waitingFor("vault-cli", forLogMessage(".*created_time.*", 1))
}
private val client = OkHttp()
private val application = greetingApplication(
MapEnvironment.from(
Properties().apply {
this[SERVER_PORT] = 0
}
)
)
@BeforeEach
fun start() {
application.start()
}
@AfterEach
fun stop() {
application.stop()
}
@Test
fun `should say hello`() {
val response = client(Request(GET, "http://localhost:${application.port()}/hello"))
assertThat(
response,
hasStatus(OK)
)
assertThat(
response,
hasBody(Regex(".+ my name is Bitelchus and my secret is watermelon"))
)
}
}
- We use Testcontainers to test with Postgres and Vault containers.
- We can override configuration values passing a
MapEnvironment
to thegreetingApplication
. In this case we only overrideSERVER_PORT
value to0
to force a random port. - 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 Vault and Database
docker compose up -d vault vault-cli db
# Start Application
./gradlew run
# Make requests
curl http://localhost:8080/hello
# Stop Application with control-c
# Stop all containers
docker compose down
Note that main class is specified in build.gradle
:
mainClassName = "org.rogervinas.GreetingApplicationKt"
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 -jar build/libs/http4k-app.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
Http4k does not support creating docker images out of the box, but for example we can create this Dockerfile
:
FROM registry.access.redhat.com/ubi8/openjdk-11:1.15
COPY build/libs/http4k-app.jar /app/
EXPOSE 8080
ENV JAVA_APP_JAR="/app/http4k-app.jar"
And then:
# Build fatjar
./gradlew shadowJar
# Build docker image and publish it to local registry
docker build . -t http4k-app
# Start Vault and Database
docker compose up -d vault vault-cli db
# Start Application
docker compose --profile http4k up -d
# Make requests
curl http://localhost:8080/hello
# Stop all containers
docker compose --profile http4k down
docker compose down
That's it! Happy coding! 💙
Top comments (0)