Tuesday, May 16, 2023

Microservices with Spring-Boot Kotlin - Cloud Native Microservices - Basic Code Structure

 We can generate the project structure from sprint initializer website like https://start.spring.io/ .

In Kotlin , Maven and Java 8 and Spring-Boot version 3.0.6 

Main Class or Entry Class :

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class MainClaApplication

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

Customer.kt:

data class Customer(var id: Int = 0, val name: String = "", val telephone: Telephone? = null) {
data class Telephone(var countryCode: String = "", var telephoneNumber: String = "")

} 

CustomerController.kt :

@RestController
class CustomerController {
@Autowired
private lateinit var customerService: CustomerService

@GetMapping(value = "/customer/{id}")
fun getCustomer(@PathVariable id: Int) =
ResponseEntity(customerService.getCustomer(id), HttpStatus.OK)

@GetMapping(value = "/customers")
fun getCustomers(@RequestParam(required = false, defaultValue = "") nameFilter: String) =
ResponseEntity(customerService.searchCustomers(nameFilter), HttpStatus.OK)

@PostMapping(value = "/customer/")
fun createCustomer(@RequestBody customerMono: Mono<Customer>) =
ResponseEntity(customerService.createCustomer(customerMono), HttpStatus.CREATED)

} 

CustomerExistException.kt :

class CustomerExistException(override val message: String) : Exception(message)

CustomerHandler.kt :

import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.BodyInserters.fromObject
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse.*
import org.springframework.web.reactive.function.server.bodyToMono
import reactor.core.publisher.onErrorResume
import java.net.URI

@Component
class CustomerHandler(val customerService: CustomerService) {
fun get(serverRequest: ServerRequest) =
customerService.getCustomer(serverRequest.pathVariable("id").toInt())
.flatMap { ok().body(fromObject(it)) }
.switchIfEmpty(status(HttpStatus.NOT_FOUND).build())

fun search(serverRequest: ServerRequest) =
ok().body(customerService.searchCustomers(serverRequest.queryParam("nameFilter")
.orElse("")), Customer::class.java)

fun create(serverRequest: ServerRequest) =
customerService.createCustomer(serverRequest.bodyToMono()).flatMap {
created(URI.create("/functional/customer/${it.id}")).build()
}.onErrorResume(Exception::class) {
badRequest().body(fromObject(ErrorResponse("error creating customer",
it.message ?: "error")))
}
}

CustomerRouter.kt :

@Component
class CustomerRouter(private val customerHandler: CustomerHandler) {
@Bean
fun customerRoutes() = router {
"/functional".nest {
"/customer".nest {
GET("/{id}", customerHandler::get)
POST("/", customerHandler::create)
}
"/customers".nest {
GET("/", customerHandler::search)
}
}
}
}

CustomerService.kt:

import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

interface CustomerService {
fun getCustomer(id: Int): Mono<Customer>
fun searchCustomers(nameFilter: String): Flux<Customer>
fun createCustomer(customerMono: Mono<Customer>): Mono<Customer>
}

CustomerServiceImpl.kt :

import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux
import reactor.core.publisher.toMono
import java.util.concurrent.ConcurrentHashMap

@Component
class CustomerServiceImpl : CustomerService {

companion object {
val initialCustomers = arrayOf(Customer(1, "Kotlin"),
Customer(2, "Spring"),
Customer(3, "Microservice", Telephone("+91", "7123456789")))
}

val customers = ConcurrentHashMap<Int, Customer>(initialCustomers.associateBy(Customer::id))

override fun getCustomer(id: Int) = customers[id]?.toMono() ?: Mono.empty()

override fun searchCustomers(nameFilter: String) = customers.filter {
it.value.name.contains(nameFilter, true)
}.map(Map.Entry<Int, Customer>::value).toFlux()

override fun createCustomer(customerMono: Mono<Customer>) =
customerMono.flatMap {
if (customers[it.id] == null) {
customers[it.id] = it
it.toMono()
} else {
Mono.error(CustomerExistException("Customer ${it.id} already exist"))
}
}
}

ErrorHandler.kt :

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class ErrorHandler {
@ExceptionHandler(Exception::class)
fun JsonParseExceptionHandler(exception: Exception): ResponseEntity<ErrorResponse> {
return ResponseEntity(ErrorResponse("JSON Error", exception.message ?: "invalid json"),
HttpStatus.BAD_REQUEST)
}
}

ErrorResponse.kt :

data class ErrorResponse(val error: String, val message: String)

in resource folder :
create one HTML file like

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
Reactive Static Content
</body>
</html>

And,

application.yml:

spring.jackson.default-property-inclusion: NON_NULL

pom.xml :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.microservices</groupId>
<artifactId>chapter4</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>chapter4</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.M7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jre8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>


</project>

This should be the ideal cloud native structure for cloud reactive microservice. In our coming blog we will further optimize it to the next level. I have used reactive frameworks like Webflux and Mono. 




No comments: