mcpField
K
kotlin-spring-boot

kotlin-spring-boot

github.com/ashchupliak开发5155 次安装48 周前更新
代码仓库

关于 kotlin-spring-boot

面向 Kotlin Spring Boot 开发者的服务与 REST API 模式参考,涵盖项目配置、数据模型、服务和控制器结构、验证、错误处理及 Kotlin 惯用写法。

主要功能

  1. 01Gradle Kotlin DSL 配置
  2. 02实体与 DTO 模式
  3. 03服务与控制器模式
  4. 04OpenAPI 接口模式
  5. 05类型化异常与全局处理
  6. 06配置属性与 Kotlin 惯用写法

适用场景

  1. 01规划 Spring Boot 服务结构
  2. 02定义经过验证的 API 请求模型
  3. 03使用 OpenAPI 描述 REST 端点
  4. 04统一处理 REST 错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ashchupliak/dream-team --skill kotlin-spring-boot

安装后在 Claude Code 中输入/kotlin-spring-boot确认已加载。

详细介绍

Kotlin Spring Boot 模式

项目配置

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.2.21"
    kotlin("plugin.spring") version "2.2.21"
    id("org.springframework.boot") version "3.5.7"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
}

实体模式

data class Environment(
    val id: UUID,
    val name: String,
    val status: EnvironmentStatus,
    val createdAt: Instant,
    val updatedAt: Instant?
)

enum class EnvironmentStatus {
    PENDING, RUNNING, STOPPED, FAILED
}

服务模式

@Service
class EnvironmentService(
    private val repository: EnvironmentRepository,
    private val computeClient: ComputeClient
) {
    // Use NEVER propagation - let caller control transaction
    @Transactional(propagation = Propagation.NEVER)
    fun create(request: CreateEnvironmentRequest): Pair<EnvironmentResponse, Boolean> {
        // Check for existing (idempotency)
        repository.findByName(request.name)?.let {
            return Pair(it.toResponse(), false) // existing
        }

        // Create new
        val environment = Environment(
            id = UUID.randomUUID(),
            name = request.name,
            status = EnvironmentStatus.PENDING,
            createdAt = Instant.now(),
            updatedAt = null
        )

        val saved = repository.save(environment)
        return Pair(saved.toResponse(), true) // created
    }

    fun findById(id: UUID): Environment =
        repository.findById(id)
            ?: throw ResourceNotFoundRestException("Environment", id)

    fun findAll(): List<Environment> =
        repository.findAll()
}

控制器模式

@RestController
class EnvironmentController(
    private val service: EnvironmentService
) : EnvironmentApi {

    override fun create(request: CreateEnvironmentRequest): ResponseEntity<EnvironmentResponse> {
        val (result, isNew) = service.create(request)
        return if (isNew) {
            ResponseEntity.status(HttpStatus.CREATED).body(result)
        } else {
            ResponseEntity.ok(result)
        }
    }

    override fun getById(id: UUID): ResponseEntity<EnvironmentResponse> =
        ResponseEntity.ok(service.findById(id).toResponse())

    override fun list(): ResponseEntity<List<EnvironmentResponse>> =
        ResponseEntity.ok(service.findAll().map { it.toResponse() })
}

API 接口模式(OpenAPI)

@Tag(name = "Environments", description = "Environment management")
interface EnvironmentApi {

    @Operation(summary = "Create environment")
    @ApiResponses(
        ApiResponse(responseCode = "201", description = "Created"),
        ApiResponse(responseCode = "200", description = "Already exists"),
        ApiResponse(responseCode = "400", description = "Validation error")
    )
    @PostMapping("/api/v1/environments")
    fun create(
        @RequestBody @Valid request: CreateEnvironmentRequest
    ): ResponseEntity<EnvironmentResponse>

    @Operation(summary = "Get environment by ID")
    @GetMapping("/api/v1/environments/{id}")
    fun getById(@PathVariable id: UUID): ResponseEntity<EnvironmentResponse>

    @Operation(summary = "List all environments")
    @GetMapping("/api/v1/environments")
    fun list(): ResponseEntity<List<EnvironmentResponse>>
}

DTO 模式

data class CreateEnvironmentRequest(
    @field:NotBlank(message = "Name is required")
    @field:Size(max = 100, message = "Name must be <= 100 chars")
    val name: String,

    @field:Size(max = 500)
    val description: String? = null
)

data class EnvironmentResponse(
    val id: UUID,
    val name: String,
    val status: String,
    val createdAt: Instant
)

// Extension function for mapping
fun Environment.toResponse() = EnvironmentResponse(
    id = id,
    name = name,
    status = status.name,
    createdAt = createdAt
)

异常处理

// Typed exceptions
throw ResourceNotFoundRestException("Environment", id)
throw ValidationRestException("Name cannot be empty")
throw ConflictRestException("Environment already exists")

// Global handler
@RestControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundRestException::class)
    fun handleNotFound(ex: ResourceNotFoundRestException): ResponseEntity<ErrorResponse> =
        ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse(ex.message ?: "Not found"))

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
        val errors = ex.bindingResult.fieldErrors.map { "${it.field}: ${it.defaultMessage}" }
        return ResponseEntity.badRequest()
            .body(ErrorResponse("Validation failed", errors))
    }
}

Kotlin 惯用写法

// Use ?.let for optional operations
user?.let { repository.save(it) }

// Use when for exhaustive matching
when (status) {
    EnvironmentStatus.PENDING -> startEnvironment()
    EnvironmentStatus.RUNNING -> return // already running
    EnvironmentStatus.STOPPED -> restartEnvironment()
    EnvironmentStatus.FAILED -> throw IllegalStateException("Cannot start failed env")
}

// Avoid !! - use alternatives
repository.findById(id).single()      // throws if not exactly one
repository.findById(id).firstOrNull() // returns null if none

// Data class copy for immutable updates
val updated = environment.copy(
    status = EnvironmentStatus.RUNNING,
    updatedAt = Instant.now()
)

配置属性

@ConfigurationProperties(prefix = "orca")
data class OrcaProperties(
    val compute: ComputeProperties,
    val timeouts: TimeoutProperties
) {
    data class ComputeProperties(
        val url: String,
        val timeout: Duration = Duration.ofSeconds(30)
    )

    data class TimeoutProperties(
        val creation: Duration = Duration.ofMinutes(5),
        val termination: Duration = Duration.ofMinutes(2)
    )
}

评论

支持 Markdown