java、Kotlin、Exposed ORM经验


`object` 和 `class` 的关键区别在于:**`class` 是蓝图,用于创建多个实例;而 `object` 是单一实体,本身就是一个唯一的、全局可用的实例**

为了更直观地理解,我们先来看一个对比表格,然后进行详细解释。

### `object` 和 `class` 主要区别对比

| 特性 | `class` (类) | `object` (对象声明) | `companion object` (伴生对象) |
| :--- | :--- | :--- | :--- |
| **核心概念** | 创建对象的**蓝图**或模板 | 实现**单例模式**的语法糖 | 属于类的**单例对象**,相当于 Java 的 `static` |
| **实例化** | 必须手动实例化 (`val obj = MyClass()`) | **无需且无法**实例化,直接通过名称访问 | **无需且无法**实例化,通过类名直接访问 |
| **数量** | 可以创建**多个**实例 | **有且只有一个**全局实例 | **有且只有一个**(依附于所属类) |
| **用途** | 定义需要多次实例化的数据结构或行为 | 工具类、全局状态容器、服务访问点 | 工厂方法、类常量、替代 Java 的静态成员 |

---

### 详细解释和代码示例

#### 1. `class` - 需要实例化的模板

一个 `class` 只是一个模板或蓝图。要使用它,你必须创建它的实例(对象)。你可以根据需要创建多个实例,每个实例都有自己的状态。

```kotlin
// 定义一个类 (蓝图)
class Car(val brand: String, var speed: Int) {
    fun accelerate() {
        speed += 10
    }
}

fun main() {
    // 创建类的多个实例
    val car1 = Car("Toyota", 0) // 使用构造函数创建实例
    val car2 = Car("BMW", 50)

    car1.accelerate()
    println(car1.speed) // 输出: 10
    println(car2.speed) // 输出: 50

    // car1 和 car2 是两个独立的对象
    println(car1 == car2) // 输出: false
}
```

#### 2. `object` - 单例(只有一个实例的对象)

一个 `object` 声明同时定义了一个类**和它的一个唯一实例**。它用于当你需要且只需要一个某种类型的对象时(单例模式)。你无法创建它的实例,直接通过对象名访问。

```kotlin
// 定义一个对象声明 (单例)
object ApplicationConfig {
    // 这些属性在整个程序中只有一份
    val version: String = "1.0.0"
    var isDebug: Boolean = true

    fun setup() {
        println("Application version $version is configured. Debug: $isDebug")
    }
}

fun main() {
    // 直接通过对象名访问,无需实例化
    println(ApplicationConfig.version) // 输出: 1.0.0
    ApplicationConfig.setup()          // 输出: Application version 1.0.0 is configured. Debug: true

    ApplicationConfig.isDebug = false // 修改全局状态

    // 在任何地方访问的都是同一个实例
    println(ApplicationConfig.isDebug) // 输出: false
}
```

**常见用途**:工具类(如 `JsonUtils`)、全局配置、服务访问点(如 `DatabaseManager`)、实现接口的单例。

#### 3. `companion object` - 类内部的单例(伴生对象)

这是一个定义在类内部的 `object`。它允许你调用其成员时**像调用 Java 的静态方法一样**,使用类名作为限定符,但这些成员仍然是真实对象的实例成员。

```kotlin
class MyClass {
    // 实例属性,每个实例都有自己的副本
    val instanceProperty: String = "I belong to an instance"

    // 伴生对象
    companion object {
        // 属于伴生对象的属性,全局只有一份
        const val CLASS_CONSTANT: String = "I belong to the class"

        fun create(): MyClass {
            // 可以访问私有构造函数
            return MyClass()
        }
    }
}

fun main() {
    // 通过类名直接访问伴生对象的成员,无需实例化 MyClass
    println(MyClass.CLASS_CONSTANT) // 输出: I belong to the class

    val obj = MyClass.create() // 调用伴生对象的工厂方法

    // 实例成员仍需通过实例访问
    println(obj.instanceProperty) // 输出: I belong to an instance

    // 错误示例:无法通过类名访问实例成员
    // println(MyClass.instanceProperty) // 编译错误!
}
```

**常见用途**- **工厂方法** (`MyClass.create()`)
- **类常量** (`MyClass.CLASS_CONSTANT`)
- **替代 Java 的静态方法和字段**
- 实现接口(伴生对象可以实现接口,而 Java 的静态方法不能)

---

### 如何选择?

- **需要创建多个具有相同结构但不同状态的对象?** → 使用 `class`。
    ```kotlin
    class User(val name: String) // 每个用户都有不同的名字
    ```

- **只需要一个全局对象来管理某些状态或提供工具函数?** → 使用 `object`。
    ```kotlin
    object Logger {
        fun log(message: String) { ... }
    }
    // 使用:Logger.log("Something happened")
    ```

- **需要一些与类相关而不是与实例相关的功能(如常量、工厂方法)?** → 在类内部使用 `companion object`。
    ```kotlin
    class ApiClient {
        companion object {
            const val BASE_URL = "https://api.example.com"
            fun newInstance(): ApiClient { ... }
        }
    }
    // 使用:ApiClient.BASE_URL, ApiClient.newInstance()
    ```

### 总结

| 关键词 | 核心思想 | 类比 |
| :--- | :--- | :--- |
| **`class`** | **蓝图** | 饼干模具 |
| **`object`** | **唯一的实物** | 唯一的、特殊的饼干 |
| **`companion object`** | **放在蓝图画旁边的唯一实物** | 和饼干模具放在一起的那个唯一的标准参考饼干 |

简单来说:用 `class` 来“批量生产”,用 `object` 来“独一无二”。

 


 

Exposed

# 代码逐行注释及 `it` 解释

```kotlin
// 定义包名,表示这个文件位于 com.example.service 包中
package com.example.service

// 导入自定义的 User 数据类
import com.example.model.User
// 导入 Exposed 框架中定义的 Users 表对象
import com.example.model.Users
// 导入 Exposed SQL DSL 的相关类
import org.jetbrains.exposed.sql.*
// 导入 Exposed 的条件表达式构建器(eq 用于创建相等比较)
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
// 导入 Exposed 的事务处理功能
import org.jetbrains.exposed.sql.transactions.transaction

// 定义一个对象声明(单例模式),包含所有用户相关的数据库操作
object UserService {

    // 创建用户函数:接收姓名和邮箱,返回新用户的ID
    fun createUser(name: String, email: String): Int = transaction {
        // 向 Users 表插入新记录
        Users.insert {
            // 这里的 it 是 Exposed 提供的 InsertStatement 类型对象
            // 它代表当前正在构建的插入语句,我们可以通过它设置要插入的字段值
            it[Users.name] = name    // 设置 name 字段的值
            it[Users.email] = email  // 设置 email 字段的值
        }[Users.id]  // 获取插入操作返回的id字段值
    }

    // 获取所有用户函数:返回用户列表
    fun getAllUsers(): List<User> = transaction {
        // 查询 Users 表中的所有记录
        Users.selectAll().map { 
            // 这里的 it 是 ResultRow 类型对象,代表查询结果中的一行数据
            // 我们将每一行数据转换为 User 对象
            toUser(it) 
        }
    }

    // 根据ID获取用户函数:接收用户ID,返回对应的用户对象或null
    fun getUserById(id: Int): User? = transaction {
        // 查询 Users 表中id等于指定值的记录
        Users.select { Users.id eq id }.singleOrNull()?.let { 
            // 这里的 it 是 ResultRow 类型对象,代表查询到的单行数据
            // 使用 let 函数将查询结果转换为 User 对象
            toUser(it) 
        }
    }

    // 更新用户函数:根据ID更新用户的姓名和邮箱,返回是否更新成功
    fun updateUser(id: Int, name: String, email: String): Boolean = transaction {
        // 更新 Users 表中id等于指定值的记录
        Users.update({ Users.id eq id }) {
            // 这里的 it 是 Exposed 提供的 UpdateStatement 类型对象
            // 它代表当前正在构建的更新语句,我们可以通过它设置要更新的字段值
            it[Users.name] = name    // 设置 name 字段的新值
            it[Users.email] = email  // 设置 email 字段的新值
        } > 0  // 更新操作返回受影响的行数,大于0表示更新成功
    }

    // 删除用户函数:根据ID删除用户,返回是否删除成功
    fun deleteUser(id: Int): Boolean = transaction {
        // 删除 Users 表中id等于指定值的记录
        Users.deleteWhere { Users.id eq id } > 0  // 删除操作返回受影响的行数,大于0表示删除成功
    }

    // 私有辅助函数:将数据库查询结果行转换为 User 对象
    private fun toUser(row: ResultRow): User = User(
        id = row[Users.id],          // 从结果行中提取id字段值
        name = row[Users.name],      // 从结果行中提取name字段值
        email = row[Users.email],    // 从结果行中提取email字段值
        createdAt = row[Users.createdAt]  // 从结果行中提取createdAt字段值
    )
}
```

## 关于 `it` 的解释

在 Kotlin 中,`it` 是一个**隐式参数名**,当 lambda 表达式只有一个参数时,可以使用 `it` 来引用这个参数,而不需要显式声明参数名。

在这段代码中,`it` 出现在几个不同的上下文中,但都是作为 lambda 表达式的单一参数:

1. **在 `insert` 和 `update` 操作中**:
   ```kotlin
   Users.insert {
       it[Users.name] = name
       it[Users.email] = email
   }
   ```
   这里的 `it` 是 `InsertStatement` 类型(对于插入)或 `UpdateStatement` 类型(对于更新),它代表了当前正在构建的 SQL 语句。通过 `it[字段] = 值` 的语法,我们设置了要插入或更新的字段值。

2. **在 `map` 和 `let` 函数中**:
   ```kotlin
   Users.selectAll().map { toUser(it) }
   ```
   这里的 `it` 是 `ResultRow` 类型,代表了数据库查询结果中的一行数据。`map` 函数对集合中的每个元素应用给定的 lambda 表达式,这里的 `it` 就是当前正在处理的元素。

3. **在 `singleOrNull()?.let` 中**:
   ```kotlin
   Users.select { Users.id eq id }.singleOrNull()?.let { toUser(it) }
   ```
   这里的 `it` 也是 `ResultRow` 类型。`let` 函数将调用它的对象作为参数传递给 lambda 表达式,这里的 `it` 就是 `singleOrNull()` 返回的结果(可能为 null)。

总结:`it` 不是固定的类型,它的类型取决于它所处的上下文。在 Kotlin 中,使用 `it` 可以简化只有一个参数的 lambda 表达式的书写,使代码更加简洁。

 

📦 项目构建命令
./gradlew build: 这是最常用的命令。它会执行完整的构建生命周期:编译源代码 → 处理资源文件 → 运行单元测试 → 打包(如生成 JAR 或 WAR 文件)。当你需要验证代码是否能完整构建并通过测试时,就用它。

./gradlew assemble: 这个命令只进行编译和打包,不会运行测试。如果你只是想快速生成一个可部署的包(例如在 CI/CD 流程中,测试已在另一阶段完成),这个命令更快。

./gradlew clean: 这个命令会删除项目根目录下的 build 文件夹,清理掉所有之前的构建产物。当你遇到一些奇怪的构建问题,或者想确保下一次构建绝对从零开始时,可以先执行它,然后再 build。

🧪 测试相关命令
./gradlew test: 运行项目中的所有单元测试。Gradle 会执行测试任务并输出结果摘要,告诉你哪些测试通过,哪些失败。

./gradlew test --tests "*MyTest*": 你可以使用 --tests 参数来指定运行特定的测试类或测试方法,支持通配符 *。这在调试单个测试时非常高效,例如 ./gradlew test --tests "*MyServiceTest*"。

📦 依赖管理命令
./gradlew dependencies: 这个命令会显示项目的依赖树。你可以清楚地看到所有直接依赖和传递性依赖,以及它们的版本。这对于排查依赖冲突(如不同版本的同名库)至关重要。

你还可以查看特定子模块的依赖(如 ./gradlew :app:dependencies)或特定配置的依赖(如 ./gradlew dependencies --configuration compileClasspath)。

./gradlew --refresh-dependencies: Gradle 会缓存下载的依赖项以提升效率。这个命令会强制 Gradle 忽略缓存,重新从远程仓库检查并下载依赖。当你更新了版本号或清理缓存后,确保获取的是最新依赖时使用。

🚀 项目运行与调试命令
./gradlew run: 如果你的 Kotlin 项目配置了 application 插件并在 build.gradle.kts 中指定了主类(mainClass),这个命令会直接编译并运行你的应用程序。

./gradlew bootRun: 对于 Spring Boot 项目(应用了 org.springframework.boot 插件),这个命令是运行应用的首选方式。它集成了 Boot 的特性,如智能类路径加载和实时热加载(如果配置了 spring-boot-devtools)。

🔍 任务查询与信息命令
./gradlew tasks: 刚接触一个项目时,运行这个命令可以列出所有可用的 Gradle 任务及其简要说明,帮你快速了解项目的构建能力。

./gradlew --version: 显示当前项目使用的 Gradle 版本、Groovy 版本、Kotlin 版本(如果用了 Kotlin DSL)以及 JVM 信息,用于环境确认。

📝 代码质量与分析命令
许多 Kotlin 项目会集成代码质量工具,这些工具通常通过 Gradle 任务调用:

./gradlew ktlintCheck: 如果你配置了 Ktlint,这个命令会检查 Kotlin 代码风格是否符合规范。

./gradlew detekt: 如果你配置了 Detekt,这个命令会进行静态代码分析,查找代码中的潜在问题、错误模式或复杂度问题。

💡 高效使用 Gradle 的技巧
使用 --daemon (守护进程): Gradle 守护进程可以显著提升后续构建的速度,因为它会缓存项目信息。现代版本的 Gradle 默认启用。

使用 --parallel: 如果你的项目包含多个子模块,这个选项可以让 Gradle并行构建这些模块,从而加快整体构建速度。

使用 --console=plain 或 --console=verbose: 在 CI/CD 环境中,你可能希望输出更简洁或更详细的日志,这个参数可以控制控制台输出的样式和内容。

使用 --info 或 --debug: 当构建失败且默认错误信息不足时,添加 --info 或 --debug 可以获取更详细的日志输出,帮助定位问题。

在 IDE 中操作: IntelliJ IDEA 等 IDE 提供了出色的 Gradle 工具窗口,你可以可视化地查看和执行任务,通常比手动输入命令更便捷。

⚠️ 常见问题与解决
依赖下载失败或缓慢: 可以考虑配置国内镜像源(如在 build.gradle.kts 或 settings.gradle.kts 的 repositories 块中添加阿里云、腾讯云等镜像仓库地址)。

构建缓存问题: 如果遇到奇怪的构建行为,尝试先运行 ./gradlew clean,然后再构建。

任务未找到: 确保你在项目的根目录下执行命令,或者使用了正确的任务路径(例如 :subproject-name:taskName)。

 

 

要生成可执行的JAR,有几种方法:

使用Application插件的分发任务:它会生成一个包含所有依赖的压缩包(tar或zip),并在其中包含一个启动脚本。但是您可能只想有一个可执行的JAR。

使用Shadow插件:这是一个常用的创建fat jar的插件。

手动配置Jar任务:将主类信息添加到清单文件,并包含所有依赖。

由于您已经使用了Application插件,但可能没有使用其分发任务,而是直接使用了jar任务,所以生成的JAR缺少清单信息。

解决方案1:使用Shadow插件(推荐)
步骤1:在build.gradle.kts中添加Shadow插件
首先,在plugins块中添加Shadow插件的引用:

kotlin
plugins {
    kotlin("jvm") version "1.9.22"
    application
    id("com.github.johnrengelman.shadow") version "8.1.1" // 添加Shadow插件
}
步骤2:重新构建
然后使用shadowJar任务构建:

bash
./gradlew shadowJar
构建完成后,会在build/libs/目录下生成一个以-all.jar结尾的fat jar,这个jar包含了所有依赖和正确的主类清单。

 

  










posted @ 2025-09-09 11:11  pearlcity  阅读(16)  评论(0)    收藏  举报