Ktor Architecture & Lifecycle
Architecture
- EngineMain Class
- Used to run the application
- Loads application.conf file
- Supported Engines:
- CIO: io.ktor.server.cio.EngineMain.main
- Jetty: io.ktor.server.jetty.EngineMain.main
- Netty: io.ktor.server.netty.EngineMain.main
- Tomcat: io.ktor.server.tomcat.EngineMain.main
- ApplicationEngine
- In charge of running the application
- Uses the configuration to decide which ports to listen to
- ApplicationEngineEnvironment
- Immutable
- Contains a Classloader, Configuration, Logger, Monitor (event bus for a port and application information that can be subscribed to), Connectors, Monitor, and installed Modules
- ApplicationCallPipeline - Contains application phases which can be intercepted
- Contains a context: ApplicationCall class
- This class has a reference to the application, the request, the response, the attributes, and parameters
- Phases:
- Setup: Prepares the call and processes and attributes
- Monitoring: Logging metrics and error handling
- Features: Infrasructure features - most features intercept at this phase
- Call: Processes a call and sends a response
- Fallback: Handles unprocessed calls
- Monitor (Event Bus):
- Raises application events
- Enables us to subscribe to the following events:
- ApplicationStarting
- ApplicationStarted
- ApplicationStopPreparing
- ApplicationStopping
- ApplicationStopped
- In between these events a database could be cleaned or emails could be sent, for ex
Ktor Routes
- Routing is a built in feature that helps us structure the page request handling
- Information about the request is extracted like the header and request parameters
- Routes are matched up against the extracted information and the route configuration
- Route functions:
- route(HttpMethod.Get, path) { do something.. }
- Shortcut functions: get, post, put, delete, head, and options
- Use trailing lambdas to create the response
- Routing tree enables us to setup complex nested routes
- Builder functions can be combined and nested
- We can trace why a certain route was chosen with the trace function
trace { application.log.trace(it.buildText()) }
- Path segments
- Optional:
/greeting/{myParamId?}
- If the path segment exists the paramater myParamId will be set to the value - Wildcard:
/weather/*/asia
- Mathes a path starting with weather and ending with asia - Tailcard:
/weather/{myParamId...}
- myParamId will be set to the rest of the URL. Can also be used without the parameter (/weather/{...}
)
- Optional:
- If there are multiple path matches the route of "highest qaulity" will be chosen
- If the header has an "Accepts"-key to perfer a type of content:
accept(ContextType.Text.Plain) { ... }
accept(ContextType.Text.Html) { ... }
accept(ContextType.Application.Json) { ... }
Builder Functions
- route(path) - segments are on the path and context is within the lambda, which could contain more routes
- method(verb) - segments on HTTP method
- param(name, value) - segments on query parameter
- param(name) - segments on query parameter key
- optionalParam(name) - segments a query parameter if it exists
- header(name, value) - segments on header content Example:
routing {
route("/weather") {
route("/asia") {
// this will only execute if the specified systemtoken is present
header("systemtoken", "weathersystem") {
handle {
call.respondText("The weather is sunny")
}
}
}
route("/europe", HttpMethod.Get) {
// if the parameter name is not present call the other handle function
param("name") {
handle {
var name = call.parameters.get("name")
call.respondText("The weather is $name")
}
}
handle {
call.respondText("The weather is rainy")
}
}
route("/usa") {
get {
call.respondText("The weather is rainy")
}
}
}
}
Sample Request:
curl -H "systemtoken: weathersystem" -X GET "localhost:8080/weather/asia"
Calling 3rd Party REST Services
- The HttpClient can be installed with different types of engines
- We can configure the HttpClient to deserialize a JSON response to an instance of a class with Gson or Jackson
- Ex using Apache:
val client = HttpClient(Apache) {
install(JsonFeature) {
serializer = GsonSerializer()
}
}
- Supported Engines:
- Apache - "io.ktor:ktor-client-apache:$ktor_version"
- Supports HTTP/1.1 and 2
- CIO - "io.ktor:ktor-client-cio:$ktor_version"
- Supports HTTP/1.x
- Jetty - "io.ktor:ktor-client-jetty:$ktor_version"
- Supports HTTP/2
- Apache - "io.ktor:ktor-client-apache:$ktor_version"
- If no engine is speicified, the default engine will be used (if any availble)
- When running in JVM, a ServiceLoader will look for an engine on the classpath and choose by sorting in alphabetical order
- On native systems (IOS, Android) an engine will be found by static linkage
Testing with the MockEngine
- The MockEngine can be used to choose a static response for a given URL, great for Unit Testing
val client = HttpClient(MockEngine)
{
engine {
addHandler { request ->
when (request.url.fullUrl) {
"https://example.org/" -> {
val responseHeaders = headersOf("Content-Type" to lostOf(ContentType.Text.Plain.toString()))
respond("Hello, world", headers = responseHeaders)
}
else -> error("Unhandled ${request.url.fullUrl}")
}
}
}
}
No Comments