1. Overview

In this tutorial, we’re going to develop a reactive microservice application with Kotlin and Spring Boot.

Our application will expose a REST API, persist data in a database and have endpoints for monitoring.

2. Use Case

These days, many of us are struggling with health problems; hence, we’ve chosen a health tracker application for our tutorial. It allows people to create their health profile and save symptoms like fever, blood pressure, etc. Later, users can see reports about their health logs.

Let’s stay safe and healthy and continue our journey.

3. Application Setup

First, we’re going to set up project dependencies. Here, we use Maven, but both Maven and Gradle are suitable for us.

Our application inherits from spring-boot-starter-parent:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/>
</parent>

Then, let’s add basic dependencies to the pom.xml that will allow us to work with Kotlin and Java:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    <version>1.8.0</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>1.8.0</version>
</dependency>

Now, we have to add dependencies for REST API and persistence.

We’ll use Spring Reactive Web for reactive REST API, so we add dependencies of spring-boot-starter-webflux and jackson-module-kotlin.

Reactive options are also available for data persistence, and we’ll use Spring Data R2DBC:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>

Now, we have to choose our database. In this tutorial, we’re using H2 as an in-memory database, but R2DBC drivers for Postgres, MySQL, and Microsoft SQL Server are also available at this time.

Here, we add the r2dbc-h2 dependency:

<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-h2</artifactId>
    <version>0.8.0.RELEASE</version>
</dependency>

At last, to finish with dependencies, we’re going to add the spring-boot-starter-actuator to provide monitoring and management API for our application.

Finally, let’s create the Application class:

@SpringBootApplication
class HealthTrackerApplication

fun main(args: Array<String>) {
    runApplication<HealthTrackerApplication>(*args)
}

We should be aware that the runApplication(*args) is a short form of SpringApplication.run(HealthTrackerApplication::class.java, *args).

4. Model

Let’s model the Profile data class, which encapsulates the user profile data:

@Table 
data class Profile(@Id var id:Long?, var firstName : String, var lastName : String,
  var birthDate: LocalDateTime)

When we use @Table without a table name, Spring generates the table name from the class name according to naming conventions. @Id is the marker for the primary key.

Then, we have the HealthRecord data class to encapsulate the health symptoms of a profile:

@Table
data class HealthRecord(@Id var id: Long?, var profileId: Long?, var temperature: Double,
  var bloodPressure: Double, var heartRate: Double, var date: LocalDate)

The HealthRecord has a dependency on the Profile class, but unfortunately, entity association is not supported by Spring Data R2DBC right now, so we’ve used the profileId instead of the Profile instance.

5. Database Configuration

At the moment, schema generation for Spring Data R2DBC is not available. So, we’ll have to do it ourselves programmatically or with script files.

Here, let’s go through the code and create a configuration class to execute the DDLs:

@Configuration
class DBConfiguration(db: DatabaseClient) {
    init {
        val initDb = db.sql{
            """ CREATE TABLE IF NOT EXISTS profile (
                    id SERIAL PRIMARY KEY,
                    //other columns specifications
                );
                CREATE TABLE IF NOT EXISTS health_record(
                    id SERIAL PRIMARY KEY,
                    profile_id LONG NOT NULL,
                    //other columns specifications
                );
            """
        }
        initDb.then().subscribe()
    }
}

Now, we’re ready to set up persistence.

6. Repositories

In this step, we’re going to create the required Repository interfaces. Let’s extend the ProfileRepository interface from the ReactiveCrudRepository:

@Repository
interface ProfileRepository: ReactiveCrudRepository<Profile, Long>

ReactiveCrudRepository provides methods like save and findById.

Then, we have the HealthRecordRepository with an extra method to return the list of health records for a profile.

Again, at the moment, query derivation is not supported with Spring Data R2DBC, and we have to write queries manually:

@Repository
interface HealthRecordRepository: ReactiveCrudRepository<HealthRecord, Long> {
    @Query("select p.* from health_record p where p.profile_id = :profileId ")
    fun findByProfileId(profileId: Long): Flux<HealthRecord>
}

7. Controllers

We need to expose the REST API to register a new profile. We also need endpoints to store and retrieve health records. For simplicity, we also reuse entities as data transfer objects.

Let’s start with ProfileController, which exposes an API for profile registration. We inject an instance of ProfileRepository via a constructor in ProfileController:

@RestController
class ProfileController(val repository: ProfileRepository) {
    
    @PostMapping("/profile")
    fun save(@RequestBody profile: Profile): Mono<Profile> = repository.save(profile)
}

Then we go through health record endpoints, one to store the data and one to return the averages.

Furthermore, let’s have the AverageHealthStatus data class, which encapsulates the average health record of a profile:

class AverageHealthStatus(var cnt: Int, var temperature: Double, 
  var bloodPressure: Double, var heartRate: Double)

And here is the HealthRecordController:

@RestController
class HealthRecordController(val repository: HealthRecordRepository) {

    @PostMapping("/health/{profileId}/record")
    fun storeHealthRecord(@PathVariable("profileId") profileId: Long, @RequestBody record: HealthRecord):
      Mono<HealthRecord> =
        repository.save(record)

    @GetMapping("/health/{profileId}/avg")
    fun fetchHealthRecordAverage(@PathVariable("profileId") profileId: Long): Mono<AverageHealthStatus> =
        repository.findByProfileId(profileId)
            .reduce( /* logic to calculate total */)
            .map { s ->
                /* logic to calculate average from count and total */
            }

}

8. Monitoring Endpoints

Spring Boot Actuator exposes endpoints to monitor and manage our application. When we add the spring-boot-starter-actuator to dependencies, by default, the /health and the /info endpoints are enabled. We can enable more endpoints by setting the value of the management.endpoints.web.exposure.include property in our application.yml.

Let’s enable /health and /metrics for our application:

management.endpoints.web.exposure.include: health,metrics

We also can enable all endpoints by setting the value to *. Be aware that exposing all endpoints may cause security risks; hence, we’d need additional security configurations.

We can see a list of all enabled actuator endpoints by calling /actuator.

To check the health status of our application, let’s call the http://localhost:8080/actuator/health. The response should be:

{
    "status": "UP"
}

which means that our application is running properly.

9. Testing

We can use WebTestClient to test our endpoints. So, as a practical sample, let’s test the profile API.

First, we create a ProfileControllerTest class and annotate it with @SpringBootTest:

@SpringBootTest
class ProfileControllerTest {}

When a class is annotated with @SpringBootTest, Spring will search in the class package and upward for a class annotated with ***@SpringBootConfiguration.**
*

Then, let’s create an instance of WebTestClient and bind it to ProfileController:

@Autowired
lateinit var controller: ProfileController

@BeforeEach
fun setup() {
    client = WebTestClient.bindToController(controller).build()
}

Now we’ve got everything ready to test our /profile endpoint:

@Test
fun whenRequestProfile_thenStatusShouldBeOk() {
    client.post()
      .uri("/profile")
      .contentType(MediaType.APPLICATION_JSON)
      .bodyValue(profile)
      .exchange()
      .expectStatus().isOk
}

10. Conclusion

We’ve finished our journey to create a microservice application with Kotlin and Spring Boot. Along the way, we learned how to expose REST API, monitor endpoints and manage persistence.

Advanced topics like security, service calls and API Gateway are for another day.

As always, the code of this tutorial is available on GitHub.


« 上一篇: Kotlin中的集合转换