diff --git a/Learning/Kotlin-Ktor.md b/Learning/Kotlin-Ktor.md index b221c5b..f620cee 100644 --- a/Learning/Kotlin-Ktor.md +++ b/Learning/Kotlin-Ktor.md @@ -8,6 +8,179 @@ Ktor 是一个比较新的、作为 KMP server 最优选的服务器后端框架 引擎选择主要是在 Netty 和 CIO 之间二选一。Netty 最完善,CIO 支持编译为字节码。如果在 JVM 上使用,Netty 功能最强大,CIO 响应更快。具体的代码编写没有差异。 +## 依赖添加方法 + +Ktor 本身的功能很少,所有的复杂功能都由依赖提供。 + +在 Ktor 里加插件一般分两步: + +### 在 `build.gradle.kts` 中添加依赖 + +```kotlin +plugins { + kotlin("jvm") version "2.0.0" + id("io.ktor.plugin") version "2.3.12" + // JSON 序列化,版本要和 Kotlin 版本一致 + id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" +} + +dependencies { + // 引擎:Netty 和 CIO 选一个就行 + implementation("io.ktor:ktor-server-netty-jvm") + // implementation("io.ktor:ktor-server-cio-jvm") + + // Ktor 基础核心 + implementation("io.ktor:ktor-server-core-jvm") + + // 常用插件:处理 JSON 请求和返回 + implementation("io.ktor:ktor-server-content-negotiation-jvm") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm") + + // 常用插件:处理跨域请求 + implementation("io.ktor:ktor-server-cors-jvm") + + // 常用插件:全局异常捕获 + implementation("io.ktor:ktor-server-status-pages-jvm") + + // 日志框架,一般用 logback + implementation("ch.qos.logback:logback-classic:1.5.6") +} +``` + +### 在代码里使用 `install` 安装 + +一般我们会给每个插件写一个单独的扩展函数,避免所有代码堆在主入口里: + +```kotlin +// 配置 JSON 解析 +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() // 这样 Ktor 就能自动把收到的 JSON 转成对应的数据类对象,或者把对象转成 JSON 返回 + } +} + +// 配置跨域 +fun Application.configureHTTP() { + install(CORS) { + anyHost() // 允许任意域名访问,上线时可以改成自己的前端域名 + allowHeader(HttpHeaders.ContentType) + } +} +``` + +## 项目文件的文件夹分类 + +一个标准的 Ktor 项目,目录结构一般长这样: + +- `src/main/kotlin/com/example/` + - `Application.kt`:程序的入口,启动服务器的地方 + - `plugins/`:插件配置目录。把各个插件的 `install` 代码分别拆成单独的 `.kt` 文件放这里(比如 `Routing.kt`, `Serialization.kt`, `HTTP.kt`) + - `routes/`:路由目录。类似其他框架里的 Controller,具体的网络接口(GET、POST 路径和参数处理)按业务拆开放这里 + - `services/`:业务逻辑目录。处理真正的业务计算、复杂操作,接口调用服务 + - `repositories/`:数据持久层。连接数据库、执行 SQL(比如用 Exposed 或原生 JDBC)放这里 + - `models/`:数据模型(也可以叫 entity、dto)。**注意:如果你在写全栈 KMP,这里的模型可以直接挪到 `core/commonMain` 里,前端和后端共用一份数据类定义。** +- `src/main/resources/` + - `application.yaml`(或者 `application.conf`):服务器的配置文件 + - `logback.xml`:控制台和文件日志输出格式 + +## 基本的编写思路 + +写 Ktor 和写传统 Springboot 后端最不一样的地方在于:它没有那么多注解和反射,完全用 Kotlin 本身的特性,因而美观而简洁。 + +### 入口文件启动服务器 + +Ktor 通常直接在 `Application.kt` 启动,main 函数不应有任何修改: + +```kotlin +package com.example + +import io.ktor.server.application.* +import io.ktor.server.netty.* + +// 配置文件里指定的入口 module +fun Application.module() { + configureSerialization() // 加载 JSON 插件 + configureHTTP() // 加载跨域插件 + configureRouting() // 注册网络接口 +} + +// 在另一个文件的 main 函数 +fun main(args: Array): Unit = EngineMain.main(args) + +``` + +### 写路由与接口(Routing) + +在 Ktor 里面,不用写复杂的 Controller 类,直接用路由块组织接口: + +```kotlin +package com.example.routes + +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import com.example.models.User + +fun Application.configureRouting() { + routing { + // 访问 GET / + get("/") { + call.respondText("Hello World!") + } + + // 可以给一组接口加上统一的前缀 + route("/api/users") { + // GET /api/users + get { + call.respond(listOf(User(id = 1, name = "Tom"))) + } + + // POST /api/users + post { + // 直接把前端传来的 json 反序列化为 User 对象 + val user = call.receive() + // 处理你的业务... + call.respond(user) + } + } + } +} +``` + +因为 Ktor 原生基于协程,所以上面的路由处理函数天然就是挂起函数(`suspend`),可以随意在里面调用挂起函数或者进行异步非阻塞操作,不用担心阻塞主线程。 + +## 配置文件的写法 + +Ktor 支持两种配置文件写法:一种是它早期的 `application.conf`(HOCON 格式),另一种是更常见的 `application.yaml`。现在建议直接使用 `application.yaml`,看起来最清楚。 + +在 `src/main/resources/application.yaml` 里面写: + +```yaml +ktor: + deployment: + port: 8080 # 服务器监听的端口 + application: + modules: + - com.example.ApplicationKt.module # 指定启动时加载哪个入口函数 + +# 这里还可以写你自己的配置项 +myApp: + jwtSecret: "12345678" + databaseUrl: "jdbc:sqlite:./data.db" +``` + +在代码里如果想读取这些配置,也很简单,注意,返回值是可空的: + +```kotlin +fun Application.module() { + // 读端口 + val port = environment.config.property("ktor.deployment.port").getString() + // 读自定义配置 + val secret = environment.config.property("myApp.jwtSecret").getString() +} +``` + ## 接下来可以看 [目录](../README.md) diff --git a/Learning/Kotlin-Springboot.md b/Learning/Kotlin-Springboot.md index 9535f55..3b2d742 100644 --- a/Learning/Kotlin-Springboot.md +++ b/Learning/Kotlin-Springboot.md @@ -10,6 +10,189 @@ Springboot(Spring) 是目前 JVM 平台功能最强大、扩展性最好、健 自带插件的话根据你的需要来选就好,如果你只是想利用 Springboot 来做一个命令行小程序也是可以的。 +## 插件与依赖 + +### `build.gradle.kts` 插件配置 + +新建项目后,你的 `plugins` 里面应该有这几项: + +```kotlin +plugins { + // 可以不用定义变量,但要保证版本一致 + val kotlinVersion = "2.0.0" + kotlin("jvm") version kotlinVersion + + // Spring 专用的 Kotlin 编译器插件 + kotlin("plugin.spring") version kotlinVersion + + id("org.springframework.boot") version "3.3.0" + id("io.spring.dependency-management") version "1.1.5" + + // 如果你用 JPA,建议加上这个,自动给数据实体类生成无参构造函数 + // kotlin("plugin.jpa") version kotlinVersion +} +``` + +### 依赖配置 + +在 `dependencies` 里,除了常规的 `spring-boot-starter-web`,还需要带上 Kotlin 的反射库: + +```kotlin +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + + // Kotlin 反射库,Spring 容器扫描注入时需要用到 + implementation("org.jetbrains.kotlin:kotlin-reflect") + + testImplementation("org.springframework.boot:spring-boot-starter-test") +} +``` + +在 Java 下通常默认用 Jackson 来处理 JSON;而在 Kotlin 下,通常更推荐配合 Kotlin 原生的序列化方案。 + +## 项目文件的文件夹分类 + +在 Springboot 里面,基本就是大家非常熟悉的经典 MVC 分层结构。在 Kotlin 里面一般也是这么分: + +- `src/main/kotlin/com/example/demo/` + - `DemoApplication.kt`:启动类,里面有 `@SpringBootApplication` 和 `main` 函数 + - `controller/`:接收前端请求的 Controller,写路由接口的地方(例如 `UserController.kt`) + - `service/`:业务接口与实现类(例如 `UserService.kt`) + - `repository/` 或 `dao/`:数据库访问层(比如 Spring Data JPA 的接口或 MyBatis 的 Mapper),如果是 Mapper 的话有时会直接写作 `Mapper`。 + - `entity/`:数据库表对应的实体类 + - `dto/`:数据传输对象(前端传进来的 Request,或者返回给前端的 Response),有时会选择放在 entity 里面 + - `config/`:全局配置类(跨域配置、Spring Security 配置等) + - `exception/`:全局异常拦截类(`@RestControllerAdvice`) +- `src/main/resources/` + - `application.yml`:全局配置文件(推荐用 yml,结构比 properties 清晰很多) + +## 基本的编写思路 + +Kotlin 写 Springboot 和 Java 最大的区别,就是 Kotlin 本身的特性减少了大量代码。 + +### 构造器注入 + +在 Java 常使用 `@Autowired`,但在 Kotlin 里面,直接用主构造函数用写变量定义即可: + +```kotlin +@Service +class UserService( + private val userRepository: UserRepository // 类对象被创建时自动注入对象 +) { + fun findUser(id: Long): UserDto { + val user = userRepository.findById(id).orElseThrow() + return UserDto(user.id, user.name) + } +} +``` + +### 用 `data class` 写 DTO 和数据载体 + +Java 会使用 Lombok 生成标准 Java Bean 类,Kotlin 的 `data class` 会自动生成 `getter/setter`、`equals`、`hashCode`、`copy`: + +```kotlin +data class CreateUserRequest( + val username: String, + val email: String? = null // 问号表示可以为 null,对应选填字段 +) + +data class UserDto( + val id: Long, + val username: String +) +``` + +### Controller + +Controller 写法和常规 Springboot 一模一样,但由于 Kotlin 支持单行表达式,写起来格外干净: + +```kotlin +@RestController +@RequestMapping("/api/users") +class UserController( + private val userService: UserService +) { + @GetMapping("/{id}") + fun getUser( + @PathVariable id: Long + ): UserDto { + return userService.findUser(id) + } + + @PostMapping + fun createUser( + @RequestBody request: CreateUserRequest + ): UserDto { + return userService.createUser(request) + } +} +``` + +### 全局异常处理 + +用 `@RestControllerAdvice` 兜底,不用在 Controller 里写 `try-catch`: + +```kotlin +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(NoSuchElementException::class) + fun handleNotFound(e: NoSuchElementException): ResponseEntity { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.message) + } +} +``` + +## 配置文件的写法 + +Springboot 支持 `.properties` 和 `.yml`,强烈推荐使用 `application.yml`。 + +在 `src/main/resources/application.yml` 里一般写: + +```yaml +server: + port: 8080 # 端口 + +spring: + application: + name: my-kotlin-app + datasource: + url: jdbc:mysql://localhost:3306/mydb + username: root + password: rootpassword + +# 自定义配置 +my-config: + token-secret: "abcdefg123456" +``` + +### 在 Kotlin 代码中读取配置 + +有两种常见方式: + +#### @Value + +适合只读取一两个简单变量: + +```kotlin +@Component +class MyComponent( + @Value("$${my-config.token-secret}") private val tokenSecret: String +) +``` + +#### @ConfigurationProperties 与 data class + +把整块配置映射成一个不可变的数据类,方便快捷: + +```kotlin +@ConfigurationProperties(prefix = "my-config") +data class MyConfigProperties( + val tokenSecret: String +) +``` +只要在启动类上加上 `@ConfigurationPropertiesScan`,就可以直接把它像普通 Service 一样注入到任何地方使用了。 + ## 接下来可以看 [目录](../README.md)