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 Spring Boot implementation, you can check the other ones in this series too.
We can create a project using Spring Initialzr and download it locally.
A lot of documentation guides at spring.io/spring-boot.
Implementation
YAML Configuration
By default, Spring Initialzr creates a template using application.properties
file. We can just rename it to application.yaml
and it will work the same.
We can put there our first configuration property:
greeting: name: "Bitelchus"
More documentation about configuration sources at Externalized Configuration and Profiles.
GreetingRepository
We will create a GreetingRepository
:
interface GreetingRepository { fun getGreeting(): String } @Repository class GreetingJdbcRepository( private val jdbcTemplate: JdbcTemplate ): GreetingRepository { fun getGreeting(): String = jdbcTemplate .queryForObject( """ SELECT greeting FROM greetings ORDER BY random() LIMIT 1 """.trimIndent(), String::class.java )!! }
- The
@Repository
annotation will make Spring Boot to create a singleton instance at startup. - We inject a
JdbcTemplate
(provided by thespring-boot-starter-jdbc
autoconfiguration) to execute queries to the database. - We use
queryForObject
and that SQL to retrieve one randomgreeting
from thegreetings
table.
Additional to spring-boot-starter-jdbc
we will need to add these extra dependencies:
implementation("org.postgresql:postgresql") implementation("org.flywaydb:flyway-core")
And the following configuration in application.yaml
:
spring: datasource: url: "jdbc:postgresql://${DB_HOST:localhost}:5432/mydb" username: "myuser" password: "mypassword" driver-class-name: "org.postgresql.Driver" flyway: enabled: true
And Flyway migrations under src/main/resources/db/migration to create and populate greetings
table.
GreetingController
We will create a GreetingController
serving /hello
endpoint:
@RestController @RequestMapping("/hello") class GreetingController( private val repository: GreetingRepository, @Value("\${greeting.name}") private val name: String, @Value("\${greeting.secret:unknown}") private val secret: String ) { @GetMapping(produces = [MediaType.TEXT_PLAIN_VALUE]) fun hello() = "${repository.getGreeting()} my name is $name" + " and my secret is $secret" }
-
@RestController
annotation will make Spring Boot to create an instance on startup and wire it properly as a REST endpoint on/hello
path, scanning its annotated methods. -
@GetMapping
will maphello
function answering toGET /hello
requests. - The controller expects a
GreetingRepository
to be injected as well as two configuration properties, no matter what property source they come from (environment variables, system properties, configuration files, Vault, ...). - 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.
GreetingApplication
As a Spring Boot requirement, we need to create a main application:
@SpringBootApplication class GreetingApplication fun main(args: Array<String>) { runApplication<GreetingApplication>(*args) }
By convention, all classes under the same package of the main application will be scanned for annotations.
Vault Configuration
We just add the dependency org.springframework.cloud:spring-cloud-starter-vault-config
and we add the following configuration in application.yaml
:
spring: cloud: vault: enabled: true uri: "http://${VAULT_HOST:localhost}:8200" authentication: "TOKEN" token: "mytoken" kv: enabled: true backend: "secret" default-context: "myapp" application-name: "myapp" config: import: optional:vault://
Then we can access the configuration property greeting.secret
stored in Vault.
You can check the documentation at Spring Vault.
Testing the endpoint
We can test the endpoint with a "slice test", meaning only the parts needed by the controller will be started:
@WebFluxTest @TestPropertySource(properties = [ "spring.cloud.vault.enabled=false", "greeting.secret=apple" ]) class GreetingControllerTest { @MockitoBean private lateinit var repository: GreetingRepository @Autowired private lateinit var client: WebTestClient @Test fun `should say hello`() { doReturn("Hello").`when`(repository).getGreeting() client .get().uri("/hello") .exchange() .expectStatus().isOk .expectBody<String>() .isEqualTo( "Hello my name is Bitelchus and my secret is apple" ) } }
- We use
WebTestClient
to execute requests to the endpoint. - We mock the repository with
@MockitoBean
. - We can use a
@TestPropertySource
to configure thegreeting.secret
property andspring.cloud.vault.enabled=false
to disable Vault.
Testing the application
To test the whole application we will use Testcontainers and the docker compose file:
@SpringBootTest(webEnvironment = RANDOM_PORT) @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)) } @Autowired private lateinit var client: WebTestClient @Test fun `should say hello`() { client .get().uri("/hello") .exchange() .expectStatus().isOk .expectBody<String>().consumeWith { assertThat(it.responseBody!!) .matches( ".+ my name is Bitelchus and my secret is watermelon" ) } } }
- We use the shared docker compose to start the required three containers.
- We use
WebTestClient
again to test the endpoint. - We use pattern matching to check the greeting, as it is random.
- As Vault is now enabled, the secret should be
watermelon
.
Test
./gradlew test
Run
# Start Vault and Database docker compose up -d vault vault-cli db # Start Application ./gradlew bootRun # Make requests curl http://localhost:8080/hello # Stop Application with control-c # Stop all containers docker compose down
Build a fatjar and run it
# Build fatjar ./gradlew bootJar # Start Vault and Database docker compose up -d vault vault-cli db # Start Application java -jar build/libs/springboot-app-0.0.1-SNAPSHOT.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
# Build docker image ./gradlew bootBuildImage # Start Vault and Database docker compose up -d vault vault-cli db # Start Application docker compose --profile springboot up -d # Make requests curl http://localhost:8080/hello # Stop all containers docker compose --profile springboot down docker compose down
That's it! Happy coding! 💙
Top comments (0)