Spring现在支持使用Kotlin来开发,可以充分利用Kotlin语言的简洁性,以及在使用Gradle插件时,也可以使用Kotlin来替代Groovy编写配置文件。
JetBrains推出的Ktor也已经到了1.4.0版本,充分利用了Kotlin语言中的协程特性和函数式编程模型。我们可以再加上R2DBC的使用,便可以构建出异步、非阻塞、反应式的整套API开发流程。
Ktor中的返回请求响应数据就是一个suspend函数,如:
suspend inline fun ApplicationCall.respond(message: Any) {
response.pipeline.execute(this, message)
}
下面就以Ktor官网的一个HTTP API教程为例子,实战一下API的编写和打包发布到Docker容器中。
使用Ktor创建API
使用IntelliJ IDEA (Ultimate版本)安装Ktor插件后,新建项目,配置 Project SDK 为 JDK 8、使用 Gradle 做依赖管理、使用 Netty 及最新版的 Ktor。
生成的项目里,src/Application.kt 文件。你会看到 Ktor 项目模板已经帮你配置好 Jackson 并生成 2 个 Route。点击 main 左边的绿色播放键将 Ktor 运行起来,打开浏览器,输入 127.0.0.1:8080,可以看到显示 HELLO WORLD! 字符串。若是开启 127.0.0.1:8080/json/jackson 则可以看到显示 key 为 hello、value 为 world 的 JSON 格式。
路由
Spring 5支持的函数式编程模型,使用RouterFunction来声明如何将请求路由到处理器代码中,在Ktor中使用routing{ }
,这是Ktor创建的DSL,进一步简化我们的编写,下面看一下对比:
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/design/taco"),this::recents)
.andRoute(POST("/design"),this::postTaco);
}
使用Ktor中的路由DSL,返回一个自定义的数据类型:
data class Snippet(val text: String)
val snippets = Collections.synchronizedList(mutableListOf(
Snippet("hello"),
Snippet("world")
))
fun Application.module() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT) // 美化输出 JSON
}
}
routing {
get("/snippets") {
call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
}
}
}
关于序列化的内容,除了上面的Jakson之外,建议使用 Kotlinx.serialization,在Kotlin 1.4 版本已经正式放出,
可以参考:JSON support using kotlinx.serialization
组合多个HTTP方法
data class PostSnippet(val snippet: PostSnippet.Text) {
data class Text(val text: String)
}
// ……
routing {
get("/snippets") {
call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
}
post("/snippets") {
val post = call.receive<PostSnippet>()
snippets += Snippet(post.snippet.text)
call.respond(mapOf("OK" to true))
}
}
我们可以使用 route(path) { }
块将具有相同前缀的路由分组。对于每个 HTTP 方法,都有一个不带路由路径参数的重载,可以用于路由的叶子节点:
routing {
route("/snippets") {
get {
call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() }))
}
post {
val post = call.receive<PostSnippet>()
snippets += Snippet(post.snippet.text)
call.respond(mapOf("OK" to true))
}
}
}
测试API
除了可以在浏览器直接输入地址端口和路径参数,还可以使用像是Postman、PostWoman或者curl。
但是IntelliJ IDEA Ultimate包含一个很好的基于编辑器的 Rest 客户端
首先创建一个HTTP Request文件,然后在文件内创建get或者post请求,可以使用上面的一些快捷菜单生成代码。
然后就可以点击方法左边的按钮运行请求,会输出请求内容和响应内容
JWT认证
gradle添加JWT:
implementation "io.ktor:ktor-auth-jwt:$ktor_version"
添加认证类:Auth
object Auth {
private const val SECRET_KEY = "secret"
private val algorithm = Algorithm.HMAC512(SECRET_KEY)
private const val issuer = "ktor.io"
private const val validityInMs = 3600*1000 * 10 // 10 hours
fun makeJwtVerifier(): JWTVerifier = JWT
.require(algorithm)
.withIssuer(issuer)
.build()
fun sign(name: String): Map<String, String> {
return mapOf("token" to makeToken(name))
}
private fun makeToken(name: String): String = JWT.create()
.withSubject("Authentication")
.withIssuer(issuer)
.withClaim("name", name)
.withExpiresAt(getExpiration())
.sign(algorithm)
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
}
在启动类中启用jwt
private val verifier = Auth.makeJwtVerifier()
install(Authentication) {
jwt {
verifier(verifier)
validate {
UserIdPrincipal(it.payload.getClaim("name").asString())
}
}
}
登陆时进行校验,校验成功后返回token
routing {
post("login") {
val user = call.receive<User>()
//TODO:校验用户、密码有效性的代码自己写
call.respond(Auth.sign(user.name))
}
}
对需要认证后才能访问的接口添加认证关键字authenticate(下面样例是用于访问secret页面的)
routing {
authenticate {
route("secret") {
get {
val user = call.authentication.principal<UserIdPrincipal>()
call.respondText("hi ${user?.name}, you are authenticated.", contentType = ContentType.Text.Plain)
}
}
}
}
API打包发布到Docker容器
使用 Gradle 打包应用
我们会使用 Gradle shadow 插件。 它将所有输出的类、资源以及所有必需的依赖项打包到一个 JAR 文件中, 并追加一个清单文件来告诉 Java 哪个是包含 main 方法的入口点主类
首先,需要在 build.gradle
文件中添加 shadow 插件依赖:
buildscript {
...
repositories {
...
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
...
classpath "com.github.jengelman.gradle.plugins:shadow:2.0.1"
}
}
之后,必须应用它以及 application 插件:
apply plugin: "com.github.johnrengelman.shadow"
apply plugin: 'application'
然后指定主类,这样它才知道当在 Docker 内部运行该 JAR 包时要运行什么:
mainClassName = 'org.sample.ApplicationKt'
该字符串是包含 main
函数的类的完整限定名。 当 main
函数是文件中的顶层函数时,类名是带有 Kt
后缀的文件名。 在上述示例中,main
函数在 org.sample
包中的 Application.kt
文件中。
最后,必须配置 shadow 插件:
shadowJar {
baseName = 'my-application'
classifier = null
version = null
}
现在可以运行 ./gradlew build
来构建并打包应用了。 应该可以在 build/libs
文件夹中获取 my-application.jar
。
关于配置这个插件的更多信息,请参见该插件的文档
因此一个完整的 build.gradle
文件会如下所示:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.github.jengelman.gradle.plugins:shadow:6.0.0"
}
}
apply plugin: 'kotlin'
apply plugin: "com.github.johnrengelman.shadow"
apply plugin: 'application'
group 'com.hellocyc'
version '0.0.1'
mainClassName = "io.ktor.server.netty.EngineMain"
sourceSets {
main.kotlin.srcDirs = main.java.srcDirs = ['src']
test.kotlin.srcDirs = test.java.srcDirs = ['test']
main.resources.srcDirs = ['resources']
test.resources.srcDirs = ['testresources']
}
repositories {
mavenLocal()
jcenter()
maven { url 'https://kotlin.bintray.com/ktor' }
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "ch.qos.logback:logback-classic:$logback_version"
implementation "io.ktor:ktor-server-core:$ktor_version"
implementation "io.ktor:ktor-jackson:$ktor_version"
implementation "io.ktor:ktor-auth-jwt:$ktor_version"
implementation "io.ktor:ktor-html-builder:$ktor_version"
testImplementation "io.ktor:ktor-server-tests:$ktor_version"
}
shadowJar {
baseName = 'my-application'
classifier = null
version = null
}
准备 Docker 镜像
在项目的根文件夹中创建一个名为 Dockerfile
的文件,其内容如下:
FROM openjdk:8-jre-alpine
ENV APPLICATION_USER ktor
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
COPY ./build/libs/my-application.jar /app/my-application.jar
WORKDIR /app
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "my-application.jar"]
我们来看下都是什么:
FROM openjdk:8-jre-alpine
这行告诉 Docker 使用以 Alpine Linux 预构建的镜像作为基础镜像。也可以使用 OpenJDK registry 中的其他镜像。Alpine Linux 的好处是镜像非常小。 我们选择的也是只有 JRE 的镜像,因为我们并不需要在镜像中编译代码,只需要运行预编译的类。
RUN mkdir /app
COPY ./build/libs/my-application.jar /app/my-application.jar
WORKDIR /app
这几行将已打包的应用复制到 Docker 镜像中,并将工作目录设置为复制后的位置。
CMD ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:InitialRAMFraction=2", "-XX:MinRAMFraction=2", "-XX:MaxRAMFraction=2", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "my-application.jar"]
最后一行指示 Docker 使用 G1 GC、4G 内存以及已打包的应用来运行 java
。
构建并运行 Docker 镜像
构建应用包:
./gradlew build
构建并标记镜像:
docker build -t my-application .
启动镜像:
docker run -m512M --cpus 2 -it -p 8080:8080 --rm my-application
通过这个命令,我们启动 Docker 进入前台模式。 它会等待服务器退出, 也会响应 Ctrl+C
来停止服务。 -it
指示 Docker 分配一个终端(tty)来管理标准输出(stdout) 并响应中断键序列。
由于我们的服务器现在是在一个隔离的容器中运行,因此我们应该告诉 Docker 暴露一个端口,以便我们可以实际访问到服务器端口。 参数 -p 8080:8080
告诉 Docker 将容器内部的 8080 端口发布为本机的 8080 端口。 因此,当告诉浏览器访问 localhost:8080
时,它会首先连向 Docker,然后为应用将其桥接到内部端口 8080
。
可以通过 -m512M
调整内存并通过 --cpus 2
调整所暴露 cpu 数。
默认情况下,容器的文件系统在容器退出后仍然存在,所以我们提供 --rm
选项以防垃圾堆积。
关于运行 docker 镜像的更多信息,请参考 docker run 文档。
上传 docker 镜像
一旦应用能在本地成功运行,那么可能就到部署它的时候了:
docker tag my-application hub.example.com/docker/registry/tag
docker push hub.example.com/docker/registry/tag
这两条命令会为 registry 创建应用的标记并将镜像上传。 当然,需要用你的 registry 的实际 URL 来替换 hub.example.com/docker/registry/tag
。
我们这里不会详细展开,因为你的配置可能需要身份认证、特殊配置甚至特殊工具。请咨询你的组织或者云平台,或者查阅 docker push 文档。
关于返回值的异步生成
在上面的Ktor应用代码中,返回数据是直接生成的,官方有一个例子:RX
在获取返回的数据时,使用Rx代码如下:
package io.ktor.samples.rx
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.reactivex.*
import kotlinx.coroutines.*
import kotlinx.coroutines.reactive.*
import java.util.concurrent.*
/**
* Documentation: https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive/kotlinx-coroutines-rx2
*/
fun main(args: Array<String>) {
embeddedServer(Netty, port = 8080) {
routing {
get("/") {
val result = Flowable.range(1, 10)
.map { it * it }
.delay(300L, TimeUnit.MILLISECONDS)
.awaitLast()
call.respondText("LAST ITEM: $result")
}
get("/iter") {
call.respondTextWriter(ContentType.Text.Plain) {
val writer = this
Flowable.range(1, 10)
.map { it * it }
.delay(300L, TimeUnit.MILLISECONDS)
.collect {
writer.write("$it,")
writer.flush()
delay(100L)
}
}
}
}
}.start(wait = true)
}
但是推荐直接使用协程API,官方也给出了使用异步的例子:
Example of Asynchronous Responses
package io.ktor.samples.async
import kotlinx.html.*
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.html.*
import io.ktor.routing.*
import kotlinx.coroutines.*
import java.util.*
import kotlin.system.*
/**
* A dedicated context for sample "compute-intensive" tasks.
*/
val compute = newFixedThreadPoolContext(4, "compute")
/**
* An alias to simplify a suspending functional type.
*/
typealias DelayProvider = suspend (ms: Int) -> Unit
/**
* The main entry point. We use @JvmOverloads so there is a signature with `Application.main()` that will be located
* via reflection. This function is referenced by the application.conf file.
*
* We pass several arguments with services and providers so we can provide mocks when doing integration tests.
*
* For more information about this file: https://ktor.io/servers/configuration.html#hocon-file
*/
@JvmOverloads
fun Application.main(random: Random = Random(), delayProvider: DelayProvider = { delay(it.toLong()) }) {
// This adds automatically Date and Server headers to each response, and would allow you to configure
// additional headers served to each response.
install(DefaultHeaders)
// This uses use the logger to log every call (request/response)
install(CallLogging)
// Registers routes
routing {
// Tabbed browsers can wait for first request to complete in one tab before making a request in another tab.
// Presumably they assume second request will hit 304 Not Modified and save on data transfer.
// If you want to verify simultaneous connections, either use "curl" or use different URLs in different tabs
// Like localhost:8080/1, localhost:8080/2, localhost:8080/3, etc
get("/{...}") {
val startTime = System.currentTimeMillis()
call.respondHandlingLongCalculation(random, delayProvider, startTime)
}
}
}
/**
* Function that will perform a long computation in a threadpool generating random numbers
* and then will respond with the result.
*/
private suspend fun ApplicationCall.respondHandlingLongCalculation(random: Random, delayProvider: DelayProvider, startTime: Long) {
val queueTime = System.currentTimeMillis() - startTime
var number = 0
val computeTime = measureTimeMillis {
// We specify a coroutine context, that will use a thread pool for long computing operations.
// In this case it is not necessary since we are "delaying", not sleeping the thread.
// But serves as an example of what to do if we want to perform slow non-asynchronous operations
// that would block threads.
withContext(compute) {
for (index in 0 until 300) {
delayProvider(10)
number += random.nextInt(100)
}
}
}
// Responds with an HTML file, generated with the kotlinx.html DSL.
// More information about this DSL: https://github.com/Kotlin/kotlinx.html
respondHtml {
head {
title { +"Ktor: async" }
}
body {
p {
+"Hello from Ktor Async sample application"
}
p {
+"We calculated number $number in $computeTime ms of compute time, spending $queueTime ms in queue."
}
}
}
}
总结
上面还未设计到R2DBC数据库的使用,后续有时间再研究补充。
对比WebFlux来编写API,个人倾向于使用函数式编程风格,减少注解的使用。Ktor的内容还有很多,本篇只是介绍了基本使用,添加HTTPS支持,以及更多的涉及请求Header、请求日志、Session等内容,仍需要研究官方博客。
虽然Ktor的学习成本高了不少(使用Kotlin语言、协程、函数式编程等),但是个人感觉这在未来会成为主流的API开发方向,毕竟Spring❤️ Kotlin and JetBrains❤️ Ktor
参考:
- Kotlin文档中关于使用Gradle插件的介绍:Using Gradle
- Spring Boot文档中关于使用Kotlin的介绍:Kotlin support
- Spring Framework 文档:Spring Framework Documentation
- Spring官网的教程汇总:Guides
- Spring官网开发“Hello World“ Restful Web服务指导:Building a RESTful Web Service
- Spring官网指导:Building REST services with Spring
- Spring官网关于使用Kotlin和Spring Boot来构建Web应用的教程:Building web applications with Spring Boot and Kotlin
- JetBrains官方博客对于Spring中的Kotlin支持情况说明:The State of Kotlin Support in Spring
- JetBrains推出的Kotr
- Ktor中文网
- ktor-samples
- jasync-sql
- r2dbc-mysql
- jasync-sql + javalin example.
- jasync-sql + ktor + coroutines example.
- jasync-sql + spring + kotlin example.
- How to implement an Object Pool with an Actor in Kotlin.
- Coroutines usage.
- Spring Data R2DBC video.
- Spring官网文档:Spring Data R2DBC
- JDBC for Spring WebFlux: Spring Data R2DBC.