Kotlin 最佳實踐

爲何寫此文

Kotlin很煩,Gralde很煩,還都是升級狂,加一塊更煩。幾個月不接觸Kotlin,再次上手時便一片迷茫。因此記錄此文,以便再次上手時查閱。html

使用Gradle建立Kotlin項目

  1. mkdir hellokt 建立項目文件夾
  2. cd hellokt 切換到項目根目錄
  3. gradle init --type java-application 使用Gradle初始化Java項目
  4. rm -rf src/main/java src/test/java gradle gradlew gradlew.bat 刪除Java目錄和GradleWrapper配置
  5. vim build.gradle 編輯Gradle項目配置
  6. mkdir -p src/main/kotlin src/test/kotlin 建立Kotlin目錄
  7. vim src/main/kotlin/App.kt 編寫Kotlin版HelloWorld
  8. gradle clean build run 使用Gradle清理、構建、運行,直接運行也可
  9. idea . 用IntelliJ IDEA 打開項目,全部選項均選擇默認,開始用IDE進行開發

爲何要用命令行建立項目?

用圖形化界面建立項目變量太多,人品很差容易掉坑裏。用命令行建立項目,能夠明確每一個文件、每行代碼的用途,整個過程可重現、可控制,還能夠避免在IDE裏某個步驟卡死半天沒反應又結束不掉的尷尬。java

爲何要刪除GradleWrapper

  • 很煩、很煩、很煩 我想安靜一下子
  • 很大、很大、很大 我硬盤不夠你折騰
  • 很慢、很慢、很慢 我知道有堵牆,不用你三天兩頭提醒

我不Care你的Gradle版本。編譯不過我天然會升級Gradle構建腳本git

build.gradlegithub

// 注意,這個文件是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.ktweb

class App {
    companion object {
        @JvmStatic
            fun main(args: Array<String>) {
                println("hello kt")
            }
    }
}

之因此將代碼放到類裏頭,是爲了支持application插件,他須要指定一個含有JVM入口靜態main方法的入口類。spring

也能夠用帶main函數的app.kt,此時mainClassName應配置爲"AppKt"docker

用Gradle構建Kotlin版的SpringBoot應用

build.gradlevim

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.ktspringboot

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 BaiJiFeiLong@gmail.com 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);
        }
    }
}

注意:websocket

主類必定要放在包裏頭(不能用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

  1. gradle build
  2. vim Dockerfile
  3. docker build --tag=hellokt .
  4. 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 BaiJiFeiLong@gmail.com 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 BaiJiFeiLong@gmail.com 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.DefaultWebSocketSession
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 BaiJiFeiLong@gmail.com 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客戶端

文章首發: http://baijifeilong.github.io/2018/11/18/kotlin-best-practice/

相關文章
相關標籤/搜索