Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Day 3 - Spring in Kotlin

Challenge - create a SpringBoot app

  • Create a SpringBoot application from scratch

Observe the pom.xml

  • What is different from what you are used to ?
          <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>
                  <groupId>org.jetbrains.kotlin</groupId>
                  <artifactId>kotlin-maven-plugin</artifactId>
                  <configuration>
                      <args>
                          <arg>-Xjsr305=strict</arg>
                      </args>
                      <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>
    

Open classes in Kotlin

  • In Kotlin, all classes are final by default
    • If you compile this
      class Try
      class Success : Try()
      
    • The compiler will fail :
      Kotlin: This type is final, so it cannot be inherited from
      
    • To make a class open for extension, we should mark that class with the open keyword :
      open class Try
      class Success : Try()
      

      This behavior can be problematic in Spring applications : some areas in Spring only work with non-final classes.

  • The natural solution is to manually open Kotlin classes using the open keyword or to use the kotlin-allopen plugin
    • Which automatically opens all classes that are necessary for Spring to work
    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-maven-allopen</artifactId>
      <version>${kotlin.version}</version>
    </dependency>
    
  • Know more about this plugin here

Analyze code structure

  • How does it differ from what you are used to ?
  • Open the DemoApplication.kt file
    • What do you notice ?
      runApplication<DemoApplication>(*args)
      

      pointers

      • No worries this is not a pointer 😜
        • * is called the spread operator
        • Allowing us to call a function taking a vararg with an array
        • We just have to prefix the array with the operator
        fun <T> asList(vararg ts: T): List<T> {
            val result = ArrayList<T>()
            for (t in ts) // ts is an Array
                result.add(t)
            return result
        }
            
        val list = listOf(1, 2, 3)
        // If you want to pass a primitive type array into vararg, you need to convert it to a regular (typed) array using the toTypedArray() function:
        asList(*list.toTypedArray())
        

Create a message Controller (RestController)

  • Return a hardcoded list of Message on GET
  • A message is composed by an id (String nullable) and a text
@RestController
class MessageController {
	@GetMapping
	fun findMessages(): List<Message> = listOf(
		Message("1", "Moi, à une époque, je voulais faire vœu de pauvreté (...) Mais avec le pognon que j'rentrais, j'arrivais pas à concilier les deux."),
		Message("2", "Au bout d'un moment, il est vraiment druide, c'mec-là, ou ça fait quinze ans qu'il me prend pour un con ?")
	)
}

data class Message(val id: String?, val text: String)

Run your application and test your Rest Controller through Postman or IntelliJ

Add the missing Layers

  • Create a Service that will contain 2 functions
    • findMessages / addMessage
    • Put a TODO in the addMessage function for now
      @Service
      class MessageService {
      fun findMessages(): List<Message> {
          TODO()
      }
      fun addMessage(message: Message) {
          TODO()
      }
      }
      
  • What else do we need to do ?

Add the Database / repository

  • This Service needs a Repository to retrieve the data
    • Create it
      interface MessageRepository: CrudRepository<Message, String> {
      @Query("SELECT * FROM messages")
      fun findMessages(): List<Message>
      }
      
  • Make our POKO representing a Table
    • Add @Table annotation and @Id on our id
      @Table("MESSAGES")
      data class Message(@Id val id: String?, val text: String)
      

Plug everything together

  • Use our repository in our Service
    • Finalize our Service implementation by saving the Message
      @Service
      class MessageService(private val repository: MessageRepository) {
      fun findMessages(): List<Message> {
          return repository.findMessages()
      }
      fun addMessage(message: Message) {
          repository.save(message)
      }
      }
      
  • Can be simplified by using IntelliJ feature : img.png
@Service
class MessageService(private val repository: MessageRepository) {
	fun findMessages(): List<Message> = repository.findMessages()
	fun addMessage(message: Message) = repository.save(message)
}
  • Use our Service in our Controller
      @RestController
      class MessageController(private val messageService: MessageService) {
          @GetMapping
          fun findMessages(): List<Message> = messageService.findMessages()
      }
    
  • Add the mapping to be able to create new messages
    @PostMapping
    fun addMessage(@RequestBody message: Message) = messageService.addMessage(message)
    

Configure the database

  • Add an application.properties file
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.url=jdbc:h2:file:./data/testdb
    spring.datasource.username=sa
    spring.datasource.password=password
    spring.datasource.schema=classpath:sql/schema.sql
    spring.datasource.initialization-mode=always
    

Add the init script

  • In the application.properties we set the schema to sql/schema.sql
    • Create this file that initializes the db schema
      CREATE TABLE IF NOT EXISTS messages
      (
      id   VARCHAR(60) DEFAULT RANDOM_UUID() PRIMARY KEY,
      text VARCHAR NOT NULL
      );
      

Use our favorite mapper (MapStruct)

  • In real life we would not expose our Entities directly from our Controller
    • Let’s add a decoupling layer
    • Change the public contract :
      • Pass a AddMessage command in the POST and return a MessageDto in the GET
// Do not pass the id -> auto-generated by our db
data class AddMessage(val text: String)
// id no more nullable
data class MessageDto(val id: String, val text: String)
  • Create our MessageMapper to map :
    • AddMessage -> Message
    • Message -> MessageDto
@Mapper
interface MessageMapper {
	fun convertToEntity(command: AddMessage): Message
	fun convertToDto(message: Message): MessageDto
}
  • Add mapstruct to our pom.xml
    • Annotation processors are supported in Kotlin with the kapt compiler plugin
    • know more about it here
<mapstruct.version>1.4.2.Final</mapstruct.version>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${mapstruct.version}</version>
</dependency>

<!-- in kotlin-maven-plugin --> 
<executions>
    <execution>
        <id>kapt</id>
        <goals>
            <goal>kapt</goal>
        </goals>
        <configuration>
            <sourceDirs>
                <sourceDir>src/main/kotlin</sourceDir>
            </sourceDirs>
            <annotationProcessorPaths>
                <annotationProcessorPath>
                    <groupId>org.mapstruct</groupId>
                    <artifactId>mapstruct-processor</artifactId>
                    <version>${mapstruct.version}</version>
                </annotationProcessorPath>
            </annotationProcessorPaths>
        </configuration>
    </execution>
</executions>
  • Use it in our Service
@Service
class MessageService(private val repository: MessageRepository) {
	private val mapper = Mappers.getMapper(MessageMapper::class.java)

	fun findMessages(): List<MessageDto> {
		return repository.findMessages()
			.map { mapper.convertToDto(it) }
	}

	fun addMessage(command: AddMessage) {
		val message = mapper.convertToEntity(command)
		repository.save(message)
	}
}

Use extension functions to hide mapper

fun AddMessage.toEntity(): Message = Mappers.getMapper(MessageMapper::class.java).convertToEntity(this)
fun Message.toDto(): MessageDto = Mappers.getMapper(MessageMapper::class.java).convertToDto(this)
  • Clean up our code :
    @Service
    class MessageService(private val repository: MessageRepository) {
      fun findMessages(): List<MessageDto> {
          return repository.findMessages()
              .map { it.toDto() }
      }
    
      fun addMessage(command: AddMessage) {
          repository.save(message.toEntity())
      }
    } 
    

Final version available in day 4