ktor 2.0的使用教程之实现log日志和jwt封装与异常处理


前言

最近找寻能快速开发的web框架,比如对py的 fastapi,go的gin以及c#的.netcore与php的laveral做了研究,php被排除的原因只是因为环境安装过于麻烦而且还容易出错(如果是php大佬勿喷),go和py其实也挺不错的,go的问题是生态与自身的问题导致开发效率并不优秀,py确实不错可惜不太懂fastapi的异常机制,最终选择使用ktor,ktor总体还可以唯一缺点是没办法对接api文档,当然如果有大佬可以完善这块欢迎私信我,目前对于api文档我采用的方案是在线api文档工具,至于使用哪个看个人喜好,这里就不提议了避免打广告的嫌疑。


一、Ktor是什么?

Ktor 是一个使用 Kotlin 以最小的成本快速创建 Web 应用程序的框架。

二、使用步骤

1.项目目录结构

├── kotlin
│   └── org
│       └── example
│           ├── Application.kt           - 入口文件
│           ├── handle                   - 项目逻辑
│           │   └── demo.kt
│           ├── plugins                  - 框架插件
│           │   ├── CallLogging.kt
│           │   ├── GlobalException.kt
│           │   ├── Jwt.kt
│           │   ├── MySqlDatabase.kt
│           │   ├── Routing.kt
│           │   └── Serialization.kt
│           └── utils                    - 项目相关工具或拓展
│               ├── AuthUtils.kt
│               ├── ext
│               │   ├── ApplicationCallConf.kt
│               │   ├── ApplicationCallLogExt.kt
│               │   └── ApplicationCallResponseExt.kt
│               └── TimeUtils.kt
└── resources                             - 项目配置文件
    ├── application.conf
    └── logback.xml

2.配置文件

  • application.conf
ktor {
    environment = dev

    deployment {
        port = 8080
        port = ${?PORT}
    }
    application {
        modules = [ org.example.ApplicationKt.module ]
    }
}

jwt {
    realm = Authentication
    subject = Authentication
    issuer = "http://0.0.0.0:8080"
    sign = "**x6g3F92c#*_"
}
  • logback.xml
<configuration>
   <!-- 定义变量 -->
   <property name="logback.logDir" value="web/logs"/>
   <property name="logback.appName" value="fyc"/>

   <!--输出到控制台-->
   <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
       <encoder>
           <pattern>%d{YYYY-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file : %line] - %msg%n</pattern>
       </encoder>
   </appender>
   <appender name="fileRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
       <!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
       <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
           <level>error</level>
       </filter>
       <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
           <!-- 每天一归档 -->
           <fileNamePattern>${logback.logDir}/error.${logback.appName}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
           <!-- 单个日志文件最多 100MB, 60天的日志周期,最大不能超过20GB,窗口大小是1到3,当保存了3个归档文件后,将覆盖最早的日志 -->
           <maxFileSize>200MB</maxFileSize>
           <maxHistory>15</maxHistory>
           <totalSizeCap>1GB</totalSizeCap>
<!--            <minIndex>1</minIndex>-->
<!--            <maxIndex>3</maxIndex>-->
       </rollingPolicy>
       <encoder>
           <pattern>%d{YYYY-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file : %line] - %msg%n</pattern>
       </encoder>
   </appender>
   <root level="INFO">
       <appender-ref ref="console"/>
       <appender-ref ref="fileRolling"/>
   </root>
   <logger name="org.eclipse.jetty" level="INFO"/>
   <logger name="io.netty" level="INFO"/>
</configuration>

3.build.gradle.kts配置

val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
val jwt_version: String by project

plugins {
    application
    kotlin("jvm") version "1.6.21"
    id("org.jetbrains.kotlin.plugin.serialization") version "1.6.21"
    id("com.github.johnrengelman.shadow") version "7.1.2" // 打包jar依赖与运行需要的配置
}

group = "org.example"
version = "0.0.1"
application {
//    mainClass.set("org.example.ApplicationKt")
    mainClass.set("io.ktor.server.netty.EngineMain") // 如果使用 EngineMain ,则需要将其配置为主类

    val isDevelopment: Boolean = project.ext.has("development")
    applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

repositories {
    mavenCentral()
    maven { url = uri("https://maven.aliyun.com/repository/public") }
    maven { url = uri("https://repo1.maven.org/maven2") }
    maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
}

dependencies {
    implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
    implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
//    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
    implementation("io.ktor:ktor-serialization-jackson:$ktor_version")
    implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")

    implementation("io.ktor:ktor-server-call-logging:$ktor_version")
    implementation("io.ktor:ktor-server-method-override:$ktor_version") // 用于解决客户端不支持put或delete请求,X-Http-Method-Override: DELETE会将请求自动转发
    implementation("io.ktor:ktor-server-status-pages:$ktor_version")

    implementation("io.jsonwebtoken:jjwt-api:$jwt_version")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:$jwt_version")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jwt_version")
}
  • gradle.properties
ktor_version=2.0.2
kotlin_version=1.6.21
logback_version=1.2.11
kotlin.code.style=official
jwt_version=0.11.5

4.项目相关代码

  • Application.kt
package org.example

import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.methodoverride.*
import org.example.plugins.*
import org.example.utils.ext.getConfString

fun main(args: Array<String>): Unit = EngineMain.main(args)

// 全局中间件在此处注册
fun Application.module() {
    // 用于解决客户端不支持put和delete请求,在请求头中使用X-Http-Method-Override标记当前真实请求
    install(XHttpMethodOverride)

    configureLogging()
    configureSerialization()
    configureException()
    configureRouting()
}
  • handle/demo.kt
package org.example.handle

import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
import org.example.plugins.JwtParameter
import org.example.plugins.JwtPlugin
import org.example.utils.AuthUtils
import org.example.utils.ext.getConfString
import org.example.utils.ext.info
import org.example.utils.ext.jsonOk

data class UserPwdDto(val name: String, val pwd: String)

fun Route.demoRoute() {
    get("/y1") {
        call.jsonOk<String>("success")
    }

    post("/login") {
        val userDto = call.receive<UserPwdDto>()
        val token = AuthUtils.instance.makeToken(call.getConfString("jwt.issuer"))
            .claim(JwtParameter.myKey, userDto.name)
            .compact()
        call.jsonOk("success", mapOf("ass_token" to token))
    }

	// 表示路由组,如: /demo/t1、/demo/all 等
    route("/demo") {
        install(JwtPlugin)
        get("t1") {
            call.jsonOk("success", "success")
        }

        get("/all") {
            call.respond(mapOf("hello2" to "world"))
        }

        get("/z2") {
            val token = call.attributes[JwtParameter.jwtKey].toString()
            call.info(token)
            call.jsonOk("success", "hello world")
        }
    }
}
  • plugins/CallLogging.kt
package org.example.plugins

import io.ktor.server.application.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.request.*

fun Application.configureLogging() {
    install(CallLogging) {
        format { call ->
            val path = call.request.path()
            val httpMethod = call.request.httpMethod.value
            val userAgent = call.request.headers["User-Agent"]

            "Path: $path, HTTPMethod: $httpMethod, UserAgent: $userAgent"
        }
    }
}
  • plugins/GlobalException.kt
package org.example.plugins

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import org.example.utils.ext.error
import org.example.utils.ext.json404
import org.example.utils.ext.json405
import org.example.utils.ext.json500

fun Application.configureException() {
    // 处理http 404 或 500等请求
    install(StatusPages) {
        // 配置500
        exception<Throwable> { call, err ->
            call.error(getMessage(err))
            call.json500(getMessage(err))
        }
        // 处理http 404
        status(HttpStatusCode.NotFound) { call, _ ->
            call.json404()
        }
        // 处理http 405
        status(HttpStatusCode.MethodNotAllowed) { call, _ ->
            call.json405()
        }
    }
}

// 获取异常全部信息
private fun getMessage(err: Throwable):String {
    val strBuffer = StringBuffer("${err.message}\n")
    for (se in err.stackTrace) {
        strBuffer.append("\tat ${se.className}(${se.fileName}:${se.lineNumber})\n")
    }
    strBuffer.deleteCharAt(strBuffer.length -1)
    strBuffer.append("}")
    return strBuffer.toString()
}
  • plugins/Jwt.kt
package org.example.plugins

import io.jsonwebtoken.JwtException
import io.ktor.server.application.*
import io.ktor.server.application.hooks.*
import io.ktor.util.*
import org.example.utils.AuthUtils
import org.example.utils.ext.getConfString
import org.example.utils.ext.json401
import org.example.utils.ext.json403
import org.example.utils.ext.warn

class JwtParameter {
    companion object {
        const val myKey = "myKey"
        val jwtKey = AttributeKey<String>(myKey)
    }
}

// token验证插件
val JwtPlugin = createRouteScopedPlugin(name = "JwtPlugin") {
    on(CallSetup) { call ->
        val authentication = call.request.headers["Authentication"]
        if (authentication.isNullOrEmpty()) {
            // 401
            call.json401("请检查是否提交验证数据")
            return@on
        }
        val authenticationArray = authentication.split(" ")
        if (authenticationArray.size != 2 || authenticationArray[0] != "Basic") {
            // 401
            call.json401("非法请求禁止访问")
            return@on
        }
        val token = authenticationArray[1]
        // 解析token信息
        try {
            val claims = AuthUtils.instance.tokenAnalysis(token)
            val iss = call.getConfString("jwt.issuer")
            // 判断发行人是否一致
            if (claims.issuer != iss) {
                call.json403("令牌不合法,禁止访问!", 10003)
                return@on
            }
            call.attributes.put(JwtParameter.jwtKey, claims[JwtParameter.myKey] as String)
        } catch (e: JwtException) {
            // 验证失败, 可能原因为jwt过期
            call.json403("token验证失败,请重新获取后尝试", 10001)
            return@on
        } catch (e: Exception) {
            call.json403("未找到token参数", 10002)
            return@on
        }
    }
}
  • plugins/MySqlDatabase.kt
package org.example.plugins

import io.ktor.server.application.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext

@OptIn(DelicateCoroutinesApi::class)
val mySqlDBPlugin = createApplicationPlugin(name = "MySqlDBPlugin") {
    val databaseContext = newSingleThreadContext("MySqlDatabaseThread")
    onCall {
        withContext(databaseContext) {
            // 数据库连接逻辑,此处需要参考官方文档
            // 链接: https://ktor.io/docs/custom-plugins.html#databases
        }
    }
}
  • plugins/Routing.kt
package org.example.plugins

import io.ktor.server.routing.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import org.example.handle.demoRoute
import java.io.File

const val BaseAPI = "/api/v1"

fun Application.configureRouting() {

    routing {
        // 配置全局静态变量
        static("$BaseAPI/") {
            staticRootFolder = File("web")
            // 这里可以设定多个 static("x"),只要想对外访问的目录统一在当前作用域下配置
            static("static") {
                files("static")
                resources(".") // 这行表示可以访问 static/x.jpg,不加没办法访问到static下的文件
            }
        }

        // 此处配置所有路由 此处路由会添加前缀 /api/v1/...
        route(BaseAPI) {
            demoRoute()
        }
    }
}

  • plugins/Serialization.kt
package org.example.plugins

import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.serialization.jackson.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.application.*

fun Application.configureSerialization() {
    install(ContentNegotiation) {
    	// 配置json解析方式, 不要采用kotlinx的json解析,kotlinx兼容性极差
        jackson {
            configure(SerializationFeature.INDENT_OUTPUT, false)
            setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
                indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
                indentObjectsWith(DefaultIndenter("  ", "\n"))
            })
//            registerModule(JavaTimeModule())  // support java.time.* types,此处会报错,官方文档给出的配置如果需要自行查阅
        }
    }
}

  • utils/AuthUtils.kt
package org.example.utils

import io.jsonwebtoken.Claims
import io.jsonwebtoken.JwtBuilder
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys

class AuthUtils private constructor() {
    private object AuthUtilsHolder {
        val holder= AuthUtils()
    }

    /** 使用静态内部类实现 */
    companion object {
        val instance = AuthUtilsHolder.holder
    }

    private val key = Keys.secretKeyFor(SignatureAlgorithm.HS512)

    // 生成token
    fun makeToken(iss: String): JwtBuilder {
        return Jwts.builder()
            .setSubject("Authentication") // 主题
            .setIssuer(iss) // 发行者
            .setIssuedAt(getTimes()) // 签名生效时间
            .setExpiration(getTimes(30 * 60 * 1000)) // 过期时间半个小时
            .signWith(key)
//            .compact() // 这里注释掉是为了灵活让外部去调用生成token字符串
    }

    // 解析token
    fun tokenAnalysis(token: String): Claims {
        val signingKey =  Jwts.parserBuilder().setSigningKey(key).build()
        return signingKey.parseClaimsJws(token).body
    }
}
  • utils/TimeUtils.kt
package org.example.utils

import java.util.*

// 获取对应处理的时间 - token
fun getTimes(validityInMs: Long = 0) = Date(System.currentTimeMillis() + validityInMs)
  • utils/ext/ApplicationCallConf.kt
package org.example.utils.ext

import io.ktor.server.application.*

// 读取配置文件 - string
fun ApplicationCall.getConfString(key: String, def: String = ""): String {
    return this.application.getConfString(key, def)
}

fun Application.getConfString(key: String, def: String = ""): String {
    return this.environment.config.propertyOrNull(key)?.getString() ?: def
}
  • utils/ext/ApplicationCallLogExt.kt
/**
 * 只为 ApplicationCall 拓展日志简化访问
 */
package org.example.utils.ext

import io.ktor.server.application.*
import io.ktor.server.request.*

private fun ApplicationCall.logStr(): String {
    val path = this.request.path()
    val httpMethod = this.request.httpMethod.value
    val userAgent = this.request.headers["User-Agent"]
    return "Path: $path, HTTPMethod: $httpMethod, UserAgent: $userAgent \n"
}

fun ApplicationCall.info(msg: String) = this.application.environment.log.info(msg)
fun ApplicationCall.info(format: String, vararg arguments: Any) = this.application.environment.log.info(format, *arguments)
fun ApplicationCall.info(msg: String, t: Throwable) = this.application.environment.log.info(msg, t)

fun ApplicationCall.debug(msg: String) = this.application.environment.log.debug(msg)
fun ApplicationCall.debug(format: String, vararg arguments: Any) = this.application.environment.log.debug(format, *arguments)
fun ApplicationCall.debug(msg: String, t: Throwable) = this.application.environment.log.debug(msg, t)

fun ApplicationCall.warn(msg: String) = this.application.environment.log.warn(msg)
fun ApplicationCall.warn(format: String, vararg arguments: Any) = this.application.environment.log.warn(format, *arguments)
fun ApplicationCall.warn(msg: String, t: Throwable) = this.application.environment.log.warn(msg, t)

fun ApplicationCall.error(msg: String) {
    this.application.environment.log.error(msg)
    this.application.environment.log.error(this.logStr())
}
fun ApplicationCall.error(format: String, vararg arguments: Any) = this.application.environment.log.error(format, *arguments)
fun ApplicationCall.error(msg: String, t: Throwable) = this.application.environment.log.error(msg, t)


fun Application.info(msg: String) = this.environment.log.info(msg)
fun Application.info(format: String, vararg arguments: Any) = this.environment.log.info(format, *arguments)
fun Application.info(msg: String, t: Throwable) = this.environment.log.info(msg, t)

fun Application.debug(msg: String) = this.environment.log.debug(msg)
fun Application.debug(format: String, vararg arguments: Any) = this.environment.log.debug(format, *arguments)
fun Application.debug(msg: String, t: Throwable) = this.environment.log.debug(msg, t)

fun Application.warn(msg: String) = this.environment.log.warn(msg)
fun Application.warn(format: String, vararg arguments: Any) = this.environment.log.warn(format, *arguments)
fun Application.warn(msg: String, t: Throwable) = this.environment.log.warn(msg, t)

fun Application.error(msg: String) = this.environment.log.error(msg)
fun Application.error(format: String, vararg arguments: Any) = this.environment.log.error(format, *arguments)
fun Application.error(msg: String, t: Throwable) = this.environment.log.error(msg, t)
  • utils/ext/ApplicationCallResponseExt.kt
// 拓展 ApplicationCall json返回
package org.example.utils.ext

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*

// 统一异常返回
data class ResponseErrorResult(val code: Int, val msg: String, val errMsg: String? = null, val errCode: Int = 0)

// 200 成功 | 失败
data class Response200Result<T: Any>(val code: Int = 200, val msg: String = "success", val errCode: Int = 0, val state: Boolean = false, val data: T? = null)


// 所有异常返回模板
suspend fun ApplicationCall.jsonError(code: HttpStatusCode, msg: String, errMsg: String? = null, errCode: Int = 0){
    respond(code, ResponseErrorResult(
        code.value,
        msg,
        errMsg,
        errCode
    ))
}

// 200 - success
suspend fun <T: Any>ApplicationCall.jsonOk(msg: String, data: T? = null) {
    respond(Response200Result(200, msg, 0, true, data=data))
}

// 200 - error
suspend fun ApplicationCall.jsonErr(msg: String, errCode: Int = 0){
    respond(Response200Result(200, msg, errCode, false, data=null))
}

// 500
suspend fun ApplicationCall.json500(errMsg: String? = null, errCode: Int = 0) {
    jsonError(HttpStatusCode.InternalServerError, "服务器内部错误", errMsg, errCode)
}

// 404
suspend fun ApplicationCall.json404(errCode: Int = 0) {
    jsonError(HttpStatusCode.NotFound, "找不到的路径", "访问的路径被删除或者不存在!", errCode)
}

// 405
suspend fun ApplicationCall.json405(errCode: Int = 0) {
    jsonError(HttpStatusCode.NotFound, "资源被禁止", "不允许使用请求行中所指定的方法", errCode)
}

// 403
suspend fun ApplicationCall.json403(msg: String, errCode: Int = 0) {
    jsonError(HttpStatusCode.Forbidden, "验证失败禁止访问", msg, errCode)
}

// 401
suspend fun ApplicationCall.json401(msg: String, errCode: Int = 0) {
    jsonError(HttpStatusCode.Unauthorized, "当前请求需要验证", msg, errCode)
}

总结

该示例已经实现 ktor + log + jwt后续拓展自己添加,相比较spring boot而言简单一些,可以使用idea编译成jar包运行需要使用shandowjar的方式打包,由于已经导入shandowjar插件直接在idea中打包即可,目前ktor相关文章较少希望大家有能力多去研究有意思的框架。

KtorKotlin 官方提供的一个轻量级、灵活的 Web 框架,它可以用于开发 Web 应用程序、API、微服务等。在 Kotlin for Desktop 中,我们可以使用 Ktor 构建和部署桌面应用程序的后端,以提供数据和服务。 下面是一个简单的 Kotlin for Desktop 中 Ktor 的使用示例: ```kotlin import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun main() { embeddedServer(Netty, port = 8080) { routing { get("/") { call.respondText("Hello, world!") } } }.start(wait = true) } ``` 这个示例创建了一个 Ktor 应用程序,监听 8080 端口,并且在访问根路径时返回 "Hello, world!" 字符串。可以在浏览器或者其他 HTTP 客户端中访问 http://localhost:8080/ 查看结果。 需要注意的是,在 Kotlin for Desktop 中使用 Ktor 可能需要添加额外的依赖项,例如: ```kotlin dependencies { implementation("io.ktor:ktor-server-netty:$ktor_version") implementation("io.ktor:ktor-server-core:$ktor_version") implementation("io.ktor:ktor-server-host-common:$ktor_version") implementation("io.ktor:ktor-serialization:$ktor_version") } ``` 其中 $ktor_version 是 Ktor 的版本号,可以根据需要进行修改。另外,还需要在项目的 build.gradle 或 settings.gradle 中添加 Maven 仓库: ```kotlin repositories { mavenCentral() } ``` 通过使用 Ktor,我们可以很方便地在 Kotlin for Desktop 中构建和部署后端服务,实现应用程序的完整功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值