Kotlin 最佳實踐
為什麼寫此文
Kotlin很煩,Gralde很煩,還都是升級狂,加一塊更煩。幾個月不接觸Kotlin,再次上手時便一片迷茫。所以記錄此文,以便再次上手時查閱。
使用Gradle建立Kotlin專案
mkdir hellokt cd hellokt gradle init --type java-application rm -rf src/main/java src/test/java gradle gradlew gradlew.bat vim build.gradle mkdir -p src/main/kotlin src/test/kotlin vim src/main/kotlin/App.kt gradle clean build run idea .
為什麼要用命令列建立專案?
用圖形化介面建立專案變數太多,人品不好容易掉坑裡。用命令列建立專案,可以明確每個檔案、每行程式碼的用途,整個過程可重現、可控制,還可以避免在IDE裡某個步驟卡死半天沒反應又結束不掉的尷尬。
為什麼要刪除GradleWrapper
- 很煩、很煩、很煩 我想安靜一會兒
- 很大、很大、很大 我硬碟不夠你折騰
- 很慢、很慢、很慢 我知道有堵牆,不用你三天兩頭提醒
我不Care你的Gradle版本。編譯不過我自然會升級Gradle構建指令碼
build.gradle
// 注意,這個檔案是Gradle構建指令碼,是指令碼,裡面的程式碼是先後執行的。至少`buildscript`要放在`apply plugin`的前面。 // 構建指令碼 buildscript { // 外掛依賴 dependencies { // Kotlin外掛對應的包 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10" } // 外掛倉庫。牆外人可直接用`mavencentral`、`jcenter` repositories { // 阿里的Maven中心倉庫映象 maven { url "https://maven.aliyun.com/repository/central"} // 阿里的jCenter映象 maven { url "https://maven.aliyun.com/repository/jcenter"} } } // 此外掛添加了 `gradle run` 命令,通過Gradle執行專案 apply plugin: 'application' // 此外掛對Kotlin語言提供了支援,可以編譯Kotlin檔案 apply plugin: 'kotlin' // application外掛run的入口class mainClassName = 'App' // 專案依賴 dependencies { // Kotlin分為兩部分,語言部分和庫部分。kotlin外掛對語言部分提供支援,`kotlin-stdlib`對庫部分提供支援。哪怕HelloWorld中使用的`println`也在庫中。所以是Kotlin專案的必選依賴 compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } // 專案倉庫 repositories { // Maven中心倉庫牆內版 maven { url "https://maven.aliyun.com/repository/central"} // jCenter中心倉庫牆內版 maven { url "https://maven.aliyun.com/repository/jcenter"} }
App.kt
class App { companion object { @JvmStatic fun main(args: Array<String>) { println("hello kt") } } }
之所以將程式碼放到類裡頭,是為了支援application外掛,他需要指定一個含有JVM入口靜態main方法的入口類。
也可以用帶main函式的app.kt,此時mainClassName應配置為"AppKt"
用Gradle構建Kotlin版的SpringBoot應用
build.gradle
buildscript { repositories { maven { url "https://maven.aliyun.com/repository/central"} maven { url "https://maven.aliyun.com/repository/jcenter"} } dependencies { classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10' classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE' } } apply plugin: 'application' apply plugin: 'kotlin' // SpringBoot外掛。Kotlin預設一切final,Spring又需要各種代理,所以需要特殊處理。同時提供`spring:bootRun`命令 apply plugin: 'org.springframework.boot' // Spring依賴管理。自動選擇依賴版本。Gradle中沒有Maven那樣內建的依賴管理(通過Parent POM 實現),需要外掛處理。 apply plugin: 'io.spring.dependency-management' mainClassName = 'bj.App' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib" // Kotlin是要在JVM裡跑的。那麼多語言特性,沒有依賴庫怎麼跑 compile "org.jetbrains.kotlin:kotlin-reflect" // 無反射不Spring。反射不在Kotlin標準庫,需單獨新增 compile 'org.springframework.boot:spring-boot-starter' // 建立單機應用所需要的最基本的Starter } repositories { maven { url "https://maven.aliyun.com/repository/central"} maven { url "https://maven.aliyun.com/repository/jcenter"} }
bj/App.kt
package bj import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.ApplicationListener /** * Created by [email protected] at 18-6-27 下午10:08 */ @SpringBootApplication open class App : ApplicationListener<ApplicationReadyEvent> { override fun onApplicationEvent(event: ApplicationReadyEvent?) { println("Ready.") } companion object { @JvmStatic fun main(args: Array<String>) { SpringApplication.run(App::class.java, *args); } } }
注意:
主類一定要放在包裡頭(不能用root或者說default),否則報java.lang.ClassNotFoundException: org.springframework.dao.DataAccessException
建立Ktor應用
build.gradle
buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10" } repositories { maven { url "https://maven.aliyun.com/repository/central"} maven { url "https://maven.aliyun.com/repository/jcenter"} } } apply plugin: 'application' apply plugin: 'kotlin' mainClassName = 'App' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" // 2. 新增Ktor依賴 compile "io.ktor:ktor-server-netty:1.0.0-beta-3" // 3.新增Logback依賴。Ktor只依賴了Slf4J,沒有Slf4J的具體實現。如果不匯入一個Slf4J的實現,將打印不出日誌來 compile "ch.qos.logback:logback-classic:1.2.3" } repositories { maven { url "https://maven.aliyun.com/repository/central"} maven { url "https://maven.aliyun.com/repository/jcenter"} // 1. 新增Ktor倉庫。沒出正式版,所以Maven中心倉沒有最新版本 maven { url "https://dl.bintray.com/kotlin/ktor" } }
bj/App.kt
import io.ktor.application.call import io.ktor.http.ContentType import io.ktor.response.respondText import io.ktor.routing.get import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty class App { companion object { @JvmStatic fun main(args: Array<String>) { val server = embeddedServer(Netty, port = 8080) { routing { get("/") { call.respondText("xx", ContentType.Text.Plain) } } } server.start(wait = true) } } }
Ktor應用打包
build.gradle
gradle build
預設打包的jar不帶Manifest,也不是FatJar,不能直接執行。新增shadow
外掛後,將多打包出一個可以直接執行的FatJar
buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10" // 1. 新增shadow外掛的依賴 classpath "com.github.jengelman.gradle.plugins:shadow:4.0.2" } repositories { maven { url "https://maven.aliyun.com/repository/central"} maven { url "https://maven.aliyun.com/repository/jcenter"} } } apply plugin: 'application' apply plugin: 'kotlin' // 2. 應用shadow外掛 apply plugin: 'com.github.johnrengelman.shadow' // 需要帶main函式的kotlin檔案main.kt或Main.kt mainClassName = 'MainKt' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compile "io.ktor:ktor-server-netty:1.0.0-beta-3" // 用於組裝HTML。非必選依賴 compile "io.ktor:ktor-html-builder:1.0.0-beta-3" compile "ch.qos.logback:logback-classic:1.2.3" } repositories { maven { url "https://maven.aliyun.com/repository/central"} maven { url "https://maven.aliyun.com/repository/jcenter"} maven { url "https://dl.bintray.com/kotlin/ktor" } }
Ktor應用安裝到Docker
gradle build vim Dockerfile docker build --tag=hellokt . docker run -it --rm -p 8080:8080 hellokt
Dockerfile
FROM openjdk:8-jre-alpine RUN mkdir /app COPY ./build/libs/hellokt-all.jar /app WORKDIR /app CMD ["java", "-jar", "hellokt-all.jar" ]
Ktor使用配置檔案(application.conf)
Ktor使用配置檔案,需要更改Application入口類,並在配置檔案中指明模組,最後通過gradle run
命令執行
main.kt
import io.ktor.application.Application import io.ktor.application.call import io.ktor.application.install import io.ktor.features.CallLogging import io.ktor.features.DefaultHeaders import io.ktor.response.respondText import io.ktor.routing.Routing import io.ktor.routing.get /** * Created by [email protected] at 18-11-18 下午12:10 */ fun Application.main() { install(DefaultHeaders) install(CallLogging) install(Routing) { get("/") { call.respondText("Hello ") } } }
application.conf
放到Resources根目錄
ktor { deployment { port = 8088 } application { modules = [MainKt.main] } }
在gradle構建指令碼中更改mainClassName
mainClassName = 'io.ktor.server.netty.EngineMain'
在Ktor專案中使用JWT
build.gradle
buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10" classpath "com.github.jengelman.gradle.plugins:shadow:4.0.2" } repositories { maven { url "https://maven.aliyun.com/repository/central" } maven { url "https://maven.aliyun.com/repository/jcenter" } } } apply plugin: 'application' apply plugin: 'kotlin' apply plugin: 'com.github.johnrengelman.shadow' mainClassName = 'io.ktor.server.netty.EngineMain' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compile "io.ktor:ktor-server-netty:1.0.0-beta-3" compile "io.ktor:ktor-html-builder:1.0.0-beta-3" compile "io.ktor:ktor-jackson:1.0.0-beta-3" compile "io.ktor:ktor-auth:1.0.0-beta-3" compile "io.ktor:ktor-auth-jwt:1.0.0-beta-3" compile "ch.qos.logback:logback-classic:1.2.3" } repositories { maven { url "https://maven.aliyun.com/repository/central" } maven { url "https://maven.aliyun.com/repository/jcenter" } maven { url "https://dl.bintray.com/kotlin/ktor" } }
main.kt
import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import com.fasterxml.jackson.databind.SerializationFeature import io.ktor.application.Application import io.ktor.application.call import io.ktor.application.install import io.ktor.auth.Authentication import io.ktor.auth.UserIdPrincipal import io.ktor.auth.authenticate import io.ktor.auth.jwt.jwt import io.ktor.auth.principal import io.ktor.features.CORS import io.ktor.features.ContentNegotiation import io.ktor.features.StatusPages import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.jackson.jackson import io.ktor.request.receive import io.ktor.response.respond import io.ktor.routing.get import io.ktor.routing.post import io.ktor.routing.route import io.ktor.routing.routing import java.util.* /** * Created by [email protected] at 18-11-18 下午12:10 */ class InvalidCredentialsException(message: String) : RuntimeException(message) data class Snippet(val user: String, val text: String) data class PostSnippet(val snippet: Text) { data class Text(val text: String) } open class SimpleJwt(val secret: String) { private val algorithm = Algorithm.HMAC256(secret) val verifier = JWT.require(algorithm).build() fun sign(name: String): String = JWT.create().withClaim("name", name).sign(algorithm) } class User(val name: String, val password: String) val users = Collections.synchronizedMap( listOf(User("test", "test")).associateBy { it.name }.toMutableMap() ) class LoginRegister(val user: String, val password: String) val snippets = Collections.synchronizedList(mutableListOf( Snippet("demo", "hello"), Snippet("demo", "world") )) fun Application.main() { //install(DefaultHeaders) //install(CallLogging) val simpleJwt = SimpleJwt("my-super-secret-for-jwt") install(ContentNegotiation) { jackson { enable(SerializationFeature.INDENT_OUTPUT) } } install(Authentication) { jwt { verifier(simpleJwt.verifier) validate { UserIdPrincipal(it.payload.getClaim("name").asString()) } } } install(StatusPages) { exception<InvalidCredentialsException> { call.respond(HttpStatusCode.Unauthorized, mapOf("OK" to false, "error" to (it.message ?: ""))) } } install(CORS) { method(HttpMethod.Options) method(HttpMethod.Get) method(HttpMethod.Post) method(HttpMethod.Put) method(HttpMethod.Delete) method(HttpMethod.Patch) header(HttpHeaders.Authorization) allowCredentials = true anyHost() } routing { route("/snippets") { authenticate { get { call.respond(mapOf("snippets" to synchronized(snippets) { snippets.toList() })) } } authenticate { post { val post = call.receive<PostSnippet>() val principal = call.principal<UserIdPrincipal>() ?: error("No principle") snippets += Snippet(principal.name, post.snippet.text) call.respond(mapOf("OK" to true)) } } } post("/login-register") { val post = call.receive<LoginRegister>() val user = users.getOrPut(post.user) { User(post.user, post.password) } if (user.password != post.password) throw InvalidCredentialsException("Invalid credentials") call.respond(mapOf("token" to simpleJwt.sign(user.name))) } } }
Ktor與Websocket
需要新增Websocket的feature:
compile "io.ktor:ktor-websockets:1.0.0-beta-3"
main.kt
import io.ktor.application.Application import io.ktor.application.install import io.ktor.http.cio.websocket.DefaultSocket/">WebSocketSession import io.ktor.http.cio.websocket.Frame import io.ktor.http.cio.websocket.readText import io.ktor.routing.routing import io.ktor.websocket.WebSockets import io.ktor.websocket.webSocket import java.util.* import java.util.concurrent.atomic.AtomicInteger import kotlin.collections.LinkedHashSet /** * Created by [email protected] at 18-11-18 下午12:10 */ class ChatClient(val session: DefaultWebSocketSession) { companion object { var lastId = AtomicInteger(0) } val id = lastId.getAndIncrement() val name = "user$id" } fun Application.main() { install(WebSockets) routing { val wsConnections = Collections.synchronizedSet(LinkedHashSet<ChatClient>()) webSocket("/chat") { val client = ChatClient(this) wsConnections += client try { while (true) { val frame = incoming.receive() when (frame) { is Frame.Text -> { val text = frame.readText() for (conn in wsConnections) { val txt = wsConnections.map { it.name }.joinToString(", ") conn.session.outgoing.send(Frame.Text(txt)) } } } } } catch (e: Exception) { println("Exception: ${e.message}") } finally { println("A connection has gone") wsConnections -= client } } } }
程式碼實現的功能:廣播訊息到每個WS客戶端
文章首發:ofollow,noindex" target="_blank">http://baijifeilong.github.io/2018/11/18/kotlin-best-practice/