使用 Kotlin 与 Spring Boot 从零搭建 Web 应用

本文参考自官方文档:Get started with Spring Boot and KotlinSpring Boot,将带你从零开始,使用 Kotlin 构建一个包含 RESTful API 和数据库交互的 Spring Boot 应用。

创建项目

IDE 选择 IDEA Community,构建系统有 Maven 和 Gradle 两种,这两种任选其一即可,本文选择的是 Gradle。

然后在官网生成我们的项目:

按照上图来配置,然后点击下方的 GENERATE 进行生成。

如果下载 Gradle 多次出错,可以将地址替换为阿里云镜像,例如:

ruby 复制代码
https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
===>
https\://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.4.1-bin.zip

构建文件

Gradle 构建文件是 build.gradle.kts,完整脚本如下:

kotlin 复制代码
// [build.gradle.kts]
plugins {
    kotlin("jvm") version "2.2.21" // 提供 Kotlin 编译支持
    kotlin("plugin.spring") version "2.2.21" // 自动为 Spring 注解的类添加 open 关键字(解决 AOP 代理报错)
    id("org.springframework.boot") version "4.0.5" // Spring Boot 核心插件(负责打包、运行)
    id("io.spring.dependency-management") version "1.1.7" // 自动管理依赖版本,下面写依赖不用加版本号
}

group = "com.example" // 项目组织标识
version = "0.0.1-SNAPSHOT" // 项目版本

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17) // 指定项目使用 JDK 17 编译和运行
    }
}

repositories {
    mavenCentral() // 指定从 Maven 中央仓库下载依赖包
    maven { url = uri("https://maven.aliyun.com/repository/public") } // 阿里云镜像仓库
}

dependencies {
    implementation("org.springframework.boot:spring-boot-h2console") // 开启 H2 数据库控制台
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc") // 轻量级数据库访问,纯 SQL
    implementation("org.springframework.boot:spring-boot-starter-webmvc") // 提供 Spring MVC 支持,用于开发 RESTful API
    implementation("org.jetbrains.kotlin:kotlin-reflect") // 解决底层反射依赖问题
    implementation("tools.jackson.module:jackson-module-kotlin") // 让 JSON 序列化完美支持 Kotlin 的空安全(?)和 data class
    runtimeOnly("com.h2database:h2") // 仅在运行时生效,引入 H2 内存数据库,常用于开发测试
    testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc-test") // 数据库层单元测试工具
    testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") // 提供 MockMvc 进行接口模拟测试
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") // Kotlin 的 JUnit 5 测试支持
    testRuntimeOnly("org.junit.platform:junit-platform-launcher") // JUnit 5 运行平台启动器
}

kotlin {
    compilerOptions {
        // 开启严格空安全检查
        freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
    }
}

tasks.withType<Test> {
    useJUnitPlatform() // 告诉 Gradle 使用 JUnit 5 引擎来执行测试任务
}

应用程序

显而易见,程序的入口就是 main/kotlin 文件夹下 DemoApplication.kt 文件中的 main() 方法:

kotlin 复制代码
// [DemoApplication.kt]
// 启用 Spring Boot 的自动配置、组件扫描
@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    // 启动应用程序
    runApplication<DemoApplication>(*args)
}

创建一个控制器

我们添加 MessageController 控制器类(控制层)来处理 Web 请求:

kotlin 复制代码
// [MessageController.kt]
@RestController // 标识这是一个 REST 控制器
class MessageController {
    @GetMapping("/") // 处理 GET 请求
    fun index(
        @RequestParam("name") // 表示请求参数为 name
        name: String // 参数值
    ) = "Hello, $name!"
}

运行应用及验证

在终端运行 ./gradlew bootRun 命令或是运行 DemoApplication.main 方法,即可运行应用程序。

在浏览器输入 http://localhost:8080?name=John 网址进行验证:看到 Hello, John! 就说明运行成功!

返回 JSON 格式数据

接下来,我们将返回的数据更新为 JSON 格式。

首先创建 Message 消息类:

kotlin 复制代码
// [Message.kt]
data class Message(
    val id: String?,
    val text: String
)

然后替换之前的 index 函数:

kotlin 复制代码
// [MessageController.kt]
@RestController
@RequestMapping("/") // 共同路径前缀
class MessageController {
    @GetMapping
    fun listMessages() = listOf(
        Message("1", "Hello!"),
        Message("2", "Bonjour!"),
        Message("3", "Privet!"),
    )
}

现在的响应将是 Message 对象集合的 JSON 数据。

由于我们引入 Jackson 序列化库,而 Spring 中的任何控制器都会默认渲染为 JSON 响应(因为使用了 @RestController 和 Spring MVC),所以该接口返回的数据就是 JSON 格式。

运行应用,访问 http://localhost:8080 网址,结果如下:

json 复制代码
[
  {
    "id": "1",
    "text": "Hello!"
  },
  {
    "id": "2",
    "text": "Bonjour!"
  },
  {
    "id": "3",
    "text": "Privet!"
  }
]

添加数据库支持

我们将使用 JDBC(Java Database Connectivity)来与数据库进行交互,为了简化演示,我们将数据访问逻辑放到业务逻辑层中(通常它位于数据访问层)。

创建 MessageService.kt

kotlin 复制代码
// [MessageService.kt]
@Service // 标记为服务层,交由 Spring 的 IoC 容器管理
class MessageService(
    private val db: JdbcTemplate // 依赖注入:Spring 自动提供 JdbcTemplate 实例
) {

    /**
     * 获取所有消息
     */
    fun findMessages(): List<Message> = db.query("select * from messages") { result, rowNum ->
        Message(result.getString("id"), result.getString("text"))
    }

    /**
     * 插入消息
     */
    fun save(message: Message): Message {
        db.update(
            "insert into messages values ( ?, ? )",
            message.id, message.text
        )
        return message
    }
}

其中 query() 函数接收一个 SQL 查询字符串和一个回调,该回调用于将数据表中的每一行数据映射为实体对象。回调的第一个参数是数据库结果集,第二个参数是当前的行数。

更新一下 MessageService 类,让它调用 Service:

kotlin 复制代码
// [MessageController.kt]
@RestController
@RequestMapping("/")
class MessageController(private val service: MessageService) {
    @GetMapping
    fun listMessages() = service.findMessages()

    /**
     * 投递一条消息
     */
    @PostMapping // 标识 POST 请求
    fun post(
        @RequestBody // 将 JSON 格式的请求体转为对象
        message: Message
    ): ResponseEntity<Message> {
        val savedMessage = service.save(message)
        // ResponseEntity 表示整个 HTTP 响应
        return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage)
    }
}

处理 ID 为空的情况

Messageid 可为空,直接存入数据库时,会引发错误。所以我们需要进行处理,生成默认的 ID:

kotlin 复制代码
// [MessageService.kt]
@Service
class MessageService(private val db: JdbcTemplate) {
    fun findMessages(): List<Message> = db.query("select * from messages") { result, _ ->
        Message(result.getString("id"), result.getString("text"))
    }

    fun save(message: Message): Message {
        val id = message.id ?: UUID.randomUUID().toString() // 如果为空,使用 UUID 作为 ID
        db.update(
            "insert into messages values ( ?, ? )",
            id, message.text
        )
        return message.copy(id = id)
    }
}

配置数据库

src/main/resources 目录下创建 schema.sql 文件,用来定义数据库。这里我们加上 DROP TABLE IF EXISTS,这样每次启动应用时数据库都会被重置,方便我们测试:

sql 复制代码
-- schema.sql
-- 重置表
DROP TABLE IF EXISTS messages;
-- 创建一个 messages 数据表
CREATE TABLE IF NOT EXISTS messages (
id       VARCHAR(60)  PRIMARY KEY,
text     VARCHAR      NOT NULL
);

打开 src/main/resources 文件夹下的 application.properties 文件,进行以下配置:

properties 复制代码
spring.application.name=demo
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./data/testdb
spring.datasource.username=name
spring.datasource.password=password
spring.sql.init.schema-locations=classpath:schema.sql
spring.sql.init.mode=always

注意,这里不要有多余的空格。

更多可查看 Spring 文档中常用的应用属性列表

使用 HTTP 客户端测试接口

我们使用 IDEA 内置的 HTTP 客户端来测试之前的接口。

在根路径创建 requests.http 文件,并添加如下请求:

http 复制代码
### Post "Hello!"
POST http://localhost:8080/
Content-Type: application/json

{
  "text": "Hello!"
}

### Post "Bonjour!"

POST http://localhost:8080/
Content-Type: application/json

{
  "text": "Bonjour!"
}

### Post "Privet!"

POST http://localhost:8080/
Content-Type: application/json

{
  "text": "Privet!"
}

### Get all the messages
GET http://localhost:8080/

依次点击执行如上请求即可。

另外,你可以通过 cURL 命令行工具来执行请求:

bash 复制代码
curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Hello!\" }"

curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Bonjour!\" }"

curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Privet!\" }"

curl -X GET --location "http://localhost:8080"

按 ID 获取单条消息

我们来添加按 ID 获取单条消息的功能:

kotlin 复制代码
// [MessageService.kt]
fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { result, _ ->
    Message(result.getString("id"), result.getString("text"))
}.singleOrNull()

MessageController 中添加新接口:

kotlin 复制代码
// [MessageController.kt]
@GetMapping("/{id}")
fun getMessage(@PathVariable id: String): ResponseEntity<Message> =
    service.findMessageById(id).toResponseEntity()

/**
 * 转为 HTTP 响应
 */
private fun Message?.toResponseEntity(): ResponseEntity<Message> =
    this?.let { ResponseEntity.ok(it) }
        ?: ResponseEntity.notFound().build() // 表示资源未找到

使用 @PathVariable 注解函数参数,可以从路径中获取对应 ID 值。

验证结果:

使用 Spring Data 简化数据访问

虽然 JdbcTemplate 很好用,但过于繁琐。我们将访问数据库的工具换为 Spring Data 的 CrudRepository,这样可以省去大量的样板代码。

首先修改 Message 类:

kotlin 复制代码
// [Message.kt]
@Table("MESSAGES") // 对应的数据表
data class Message(
    @Id val id: String? = null,
    val text: String
)

声明一个与 Message 数据类进行交互的 MessageRepository 接口:

kotlin 复制代码
// [MessageRepository.kt]
interface MessageRepository : CrudRepository<Message, String>

CrudRepository 接口内部已经封装好了基础的 CRUD 方法。

最后修改 MessageService 类,让它调用 MessageRepository,而不是执行 SQL 语句:

kotlin 复制代码
// [MessageService.kt]
@Service
class MessageService(private val db: MessageRepository) {
    fun findMessages(): List<Message> = db.findAll().toList()

    fun findMessageById(id: String): Message? = db.findByIdOrNull(id)

    fun save(message: Message): Message = db.save(message)
}

因为不再使用 JdbcTemplate,也没有在代码中对 ID 进行判空,因此我们需要更新表的定义,让插入对象时数据库自动生成默认的 UUID。

修改 schema.sql

sql 复制代码
-- schema.sql
DROP TABLE IF EXISTS messages;
CREATE TABLE IF NOT EXISTS messages (
    id      VARCHAR(60)  DEFAULT RANDOM_UUID() PRIMARY KEY,
    text    VARCHAR      NOT NULL
);

写在最后:一个真实的后端项目还会有哪些东西?

在真实的后端项目中,还会有以下内容:

  • 安全与鉴权 (Security) :我们的基础应用是"裸奔"的。真实项目会引入 Spring Security,并结合 JWT (JSON Web Token) 或 OAuth2 来进行用户登录认证、接口权限拦截。

  • 全局异常处理 :我们目前如果报错,Spring 会返回默认的错误页面。实际开发中会使用 @RestControllerAdvice@ExceptionHandler 统一拦截异常,返回标准格式的 JSON 错误码。

  • 参数校验 (Validation) :在 Controller 接收请求体时,真实项目会使用 spring-boot-starter-validation 配合 @Valid@NotBlank 等注解,在进入业务逻辑前过滤掉不合法的输入。

  • 分层架构更加严格:我们的例子为了简便,没有剥离独立的 DAO 层或 DTO。真实项目往往有 Controller(接口层)、Service(业务逻辑层)、Repository/DAO(数据访问层),并在各层之间使用 DTO(数据传输对象)和 VO(视图对象)进行数据隔离,防止数据库实体结构直接暴露给前端。

  • 日志管理:不会仅仅依赖控制台输出,而是引入 Logback 等工具,将日志分级别(INFO, WARN, ERROR)记录到文件中,并支持日志的滚动切分。

  • 更强大的数据库及迁移工具:开发环境用 H2 没问题,生产环境通常是 MySQL 或 PostgreSQL。为了管理数据库版本的变更,还会引入 Flyway 或 Liquibase 这样的数据库版本控制工具。

  • 缓存与性能优化:对于高频访问的数据,会引入 Redis 这样的内存数据库做缓存,减轻底层关系型数据库的压力。

相关推荐
Fate_I_C1 小时前
Adroid Data Binding数据绑定对比(findViewXX、ButterKnife)
android·kotlin·databinding
一 乐1 小时前
交通感知与车路协同系统|基于springboot + vue交通感知与车路协同系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·交通感知与车路协同系统
uElY ITER1 小时前
基于Spring Boot 3 + Spring Security6 + JWT + Redis实现登录、token身份认证
spring boot·redis·spring
book123_0_992 小时前
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
java·spring boot·后端
NCIN EXPE2 小时前
使用Springboot + netty 打造聊天服务(一)
java·spring boot·后端
MXN_小南学前端2 小时前
Vue3 + Spring Boot 工单系统实战:用户反馈和客服处理的完整闭环(提供gitHub仓库地址)
前端·javascript·spring boot·后端·开源·github
smileNicky3 小时前
Spring AI系列之基于MCP协议实现天气预报工具插件
人工智能·spring boot·spring
Fate_I_C4 小时前
Android DataBinding数据绑定表达式、双向绑定
android·kotlin·databinding
一條狗4 小时前
学习日报 20260423|[特殊字符] 深度解析:Vue 3 SPA 部署到 Spring Boot 的 404/500 错误排查与完美解决方案-2
vue.js·spring boot·学习