Micronaut Kotlin Integrations

Includes extensions to Micronaut for Kotlin

Version:

1 Introduction

This project provides various extensions and improvements to the Micronaut and Kotlin experience.

Release History

1.0.0.RC1

  • First version of micronaut-kotlin-extension-functions module

1.0.0.M2

  • Support for auto config of Jackson Kotlin Module even if module scan is disabled

  • Improved separation of route and application building in Kotr support

  • Dependency Upgrades

1.0.0.M1

  • Initial Milestone

2 Kotlin Runtime Support

The micronaut-kotlin-runtime dependency adds the following features:

  • Support for defining configuration with config4k.

  • A runtime dependency on jackson-module-kotlin

To enable the above features add the dependency to your build:

implementation("io.micronaut.kotlin:micronaut-kotlin-runtime:1.0.1")
<dependency>
    <groupId>io.micronaut.kotlin</groupId>
    <artifactId>micronaut-kotlin-runtime</artifactId>
    <version>1.0.1</version>
</dependency>

Config4k Support

Config4k is a type safe configuration format for Kotlin based on HOCON (Human-Optimized Config Object Notation). Configuration files are defined using the conf extension. The following an example configuration file:

An Example src/main/resources/application.conf
micronaut {
  server {
    port = 8081
  }
}

test-property = "bad-value"

custom {
  user = ${USER}
}

3 Ktor Support

Ktor is a Kotlin framework for building connected applications. The micronaut-ktor module includes support for using Ktor as the server instead of Micronaut’s native HTTP server.

This allows users familiar with Ktor to use Micronaut features such as Dependency Injection, AOP, configuration management and so on.

See the Example application which demonstrates how to setup a Micronaut Ktor application

3.1 The KtorApplication class

The entry point for a Micronaut Ktor application is an Application class. An example class can be seen below:

Example Application class
import io.ktor.server.netty.NettyApplicationEngine
import io.micronaut.ktor.*
import org.slf4j.LoggerFactory
import javax.inject.Singleton

@Singleton
class Application : KtorApplication<NettyApplicationEngine.Configuration>({ (1)
    applicationEngineEnvironment { (2)
        log = LoggerFactory.getLogger(Application::class.java)
    }

    applicationEngine { (3)
        workerGroupSize = 10
    }
})

fun main(args: Array<String>) { (4)
    runApplication(args)
}
1 The Application class extends io.micronaut.ktor.KtorApplication and provides the server type (in this case Netty).
2 You can optionally configure the ApplicationEngineEnvironment.
3 You can optionally configure the ApplicationEngine. In this case NettyApplicationEngine instance’s workerGroupSize is set to 10
4 A main method is defined for running the application within a runnable JAR

3.2 Defining Ktor Modules

To define Ktor modules you can create classes that subclass io.micronaut.ktor.KtorApplicationBuilder to install features or io.micronaut.ktor.KtorRoutingBuilder to configure routes.

For example, the following will install the Jackson feature (when ktor-jackson is on the classpath):

Installing Features
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.jackson.jackson
import io.micronaut.ktor.KtorApplicationBuilder
import javax.inject.Singleton

@Singleton
class GreetingConfiguration : KtorApplicationBuilder({ (1)
    install(ContentNegotiation) { (2)
        jackson {
            enable(SerializationFeature.INDENT_OUTPUT)
        }
    }
})
1 The class subclasses KtorApplicationBuilder.
2 The ContentNegotiation feature is installed and Jackson configured.

To build application routes you can use the KtorRoutingBuilder:

Defining Routes
import io.ktor.application.call
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.*
import io.micronaut.ktor.KtorRoutingBuilder
import javax.inject.Singleton

@Singleton
class GreetingRoutes(private val greetingService: GreetingService) : KtorRoutingBuilder({ (1)
    get("/") {
        call.respond(greetingService.greet())
    }

    post("/") {
        val name = call.receive<CustomGreetingRequest>().name

        call.respond(greetingService.greet(name))
    }
})

data class CustomGreetingRequest(val name: String)
1 The class subclasses KtorRoutingBuilder and uses dependency injection to reference GreetingService.
2 The routing DSL is used to build the application routes.

4 Kotlin Extension Functions

The micronaut-kotlin-extension-functions dependency adds a variety of convenience functions to make using micronaut with kotlin more user-friendly.

implementation("io.micronaut.kotlin:micronaut-kotlin-extension-functions:1.0.1")
<dependency>
    <groupId>io.micronaut.kotlin</groupId>
    <artifactId>micronaut-kotlin-extension-functions</artifactId>
    <version>1.0.1</version>
</dependency>

For example, reified type parameters help alleviate the need for using ::class.java in many places where it would otherwise be required.

Thus, through defining a reified extension function to something like BlockingHttpClient like so:

Example HttpClient Extensions
inline fun <reified T: Any> BlockingHttpClient.retrieveObject(request: HttpRequest<Any>): T =
        retrieve(request, argumentOf<T>())

inline fun <reified T: Any> BlockingHttpClient.retrieveList(request: HttpRequest<Any>): List<T> =
        retrieve(request, argumentOfList<T>())

We are able to use the client a little more succinctly, as shown in this test:

Test Demonstrating Client Extension usage
val embeddedServer = ApplicationContext.run(EmbeddedServer::class.java)
val client = embeddedServer.applicationContext.createBean(HttpClient::class.java, embeddedServer.url).toBlocking()

// Test single object retrieve extension
val getOneConventional = client.retrieve(HttpRequest.GET<Any>("/heroes/any"), Argument.of(Hero::class.java))
val getOneReified = client.retrieveObject<Hero>(HttpRequest.GET<Any>("/heroes/any"))
assertEquals(getOneConventional, getOneReified)

// Test list retrieve extension
val heroListConventional = client.retrieve(HttpRequest.GET<Any>("/heroes/list"), Argument.listOf(Hero::class.java))
assertEquals(heroListConventional.size, 3)
assertTrue(heroListConventional.find { it.alterEgo == "Diana Prince" } != null) // Let's make sure Wonder Woman is there!
val heroListReified = client.retrieveList<Hero>(HttpRequest.GET<Any>("/heroes/list"))
assertEquals(heroListConventional, heroListReified)
val heroListByType : List<Hero> = client.retrieveList(HttpRequest.GET<Any>("/heroes/list"))
assertEquals(heroListByType, heroListReified)

As another example, you can use a generic startApplication<AppClass> with a configuration lambda and generic mapError<ExceptionClass>, as you would might use Micronaut.build().mainClass(App::class.java).start().

An Example src/main/kotlin/SimpleApplication.kt using the extension function.
object Application

fun main(args: Array<String>) {
    startApplication<Application>(*args) {
        packages("org.example.app")
        mapError<RuntimeException> { 500 }
    }
}
You will need to import the functions like this: import io.micronaut.kotlin.FUNCTION_NAME

Full documentation of the provided extension functions can be found via the dokka docs for this project.

Do be aware when defining extension functions in this project or your own, an extension that shadows a member function may have unexpected behavior, and does not throw a compiler error but rather warns. See this Kotlin discussion topic for the latest information.