本文参考自官方文档:Get started with Spring Boot and Kotlin,Spring 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 为空的情况
Message 的 id 可为空,直接存入数据库时,会引发错误。所以我们需要进行处理,生成默认的 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 值。
验证结果:
- 访问 http://localhost:8080/ 获取全部列表。
- 访问 http://localhost:8080/填入某条消息的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 这样的内存数据库做缓存,减轻底层关系型数据库的压力。