完善 Ktor 与 Spring Boot 基础文档
This commit is contained in:
@@ -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<String>): 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<User>()
|
||||
// 处理你的业务...
|
||||
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)
|
||||
|
||||
@@ -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<String> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user