跳到主要内容

SpringBoot Kotlin 中优雅处理 DTO 非空校验

· 阅读需 6 分钟
chihaicheng
Front End Engineer

Spring Boot Kotlin 中优雅处理 DTO 非空校验:属性委托方案

在 Spring Boot Kotlin 开发中,如何处理 DTO 的非空校验同时保持代码清晰度?本文将介绍传统方案的痛点,并展示如何使用 Kotlin 属性委托实现优雅的解决方案。

问题

在 Spring Boot Java 项目中,我们通常这样处理 DTO 的非空校验:

public class UserDTO {
@NotNull(message = "用户名不能为空")
private String username;

// getter 和 setter
}

但在 Kotlin 中,我们期望写出更符合空安全特性的代码:

class UserDTO {
@field:NotBlank(message = "用户名不能为空")
var username: String = ""
}

然而这种方式存在两个主要问题:

  1. 原始类型(Int, Long 等)无法使用 lateinit
  2. 默认值会绕过 @NotNull 校验
  3. 他人看到迷惑 容易引起歧义

原始类型问题

对于原始类型,我们无法使用 lateinit

class ActionDTO {
// 编译错误:'lateinit' modifier is not allowed on primitive types
@field:NotNull
lateinit var actionType: Int
}

默认值问题

当使用默认值时,空值会被默认值替换,导致校验失效:

class TestDTO {
@field:NotNull(message = "type 不能为空")
var type: Int = 0 // 默认值会导致空请求通过校验
}

// 请求 {} 会通过校验,type=0

解决方案:属性委托 + 注解

Kotlin 属性委托机制的解决方案:

1. 创建非空委托类

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
* 非空属性委托
* 确保属性在使用前已被初始化
*/
class NotNullVar<T : Any>() : ReadWriteProperty<Any, T> {
private var value: T? = null

override fun getValue(thisRef: Any, property: KProperty<*>): T {
return value ?: throw IllegalStateException(
"${property.name} 未初始化"
)
}

override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
this.value = value
}
}

2. 在 DTO 中使用委托属性

import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull

class UserDTO {
@get:NotBlank(message = "用户名不能为空")
var username: String by NotNullVar()

@get:NotNull(message = "年龄不能为空")
var age: Int by NotNullVar() // 原始类型也可用

@get:Email(message = "邮箱格式不正确")
var email: String by NotNullVar()
}

关键点

  • 使用 @get:注解 而非 @field:注解
  • 委托属性同时适用于原始类型和引用类型
  • 业务代码中可直接使用非空值

3. Controller 中使用

@RestController
@RequestMapping("/users")
class UserController {

@PostMapping
fun createUser(@Valid @RequestBody userDTO: UserDTO): User {
// 直接使用非空属性(无需安全操作符)
val user = User(
name = userDTO.username,
age = userDTO.age,
email = userDTO.email
)

return user
}
}

4. 改变 jackson 配置

spring:
jackson:
deserialization:
fail-on-null-for-primitives: true

原因

Kotlin 反序列化时对基本类型(Int、Long、Boolean 等)的 null 值会有默认值策略。

Spring Boot 默认使用 Jackson 来把 JSON 转成对象,当 JSON 里 {"type": null} 并且你的属性是非可空(Int)时,Jackson 会把它当成基本类型的默认值,也就是 0。

fail-on-null-for-primitives: true 抛异常就行了 然后再捕获异常就行

方案优势

  1. 代码清晰度

    • 属性声明为非空类型
    • 业务代码无需安全操作符(!!?.
    • 避免可空类型引起的歧义
  2. 校验有效性

    • 正确处理原始类型校验
    • 不会因默认值绕过校验
    • 支持所有标准校验注解
  3. 类型安全

    • 编译时检查属性访问
    • 运行时确保非空
    • 减少空指针异常风险
  4. 扩展性强

    // 自定义校验注解
    @get:PositiveOrZero(message = "积分不能为负")
    var points: Int by NotNullVar()

    // 集合校验
    @get:Size(min = 1, message = "至少选择一个爱好")
    var hobbies: List<String> by NotNullVar()

与传统方案对比

特性传统方案 (可空类型)委托方案
属性声明var age: Int? = nullvar age: Int by NotNullVar()
业务代码使用userDTO.age!!userDTO.age
原始类型支持
默认值问题存在不存在
代码可读性较低(属性可空但业务非空)高(属性始终非空)
校验失败触发
额外运行时依赖

常见问题解答

1. 为什么使用 @get: 而不是 @field:

在委托属性中:

  • @field: 作用于字段,但委托属性没有直接字段
  • @get: 作用于 getter 方法,Spring 校验通过 getter 访问值
  • @delegate: 也可用,但 @get: 更通用

Android 状态栏颜色设置在jetpack compose中

· 阅读需 2 分钟
chihaicheng
Front End Engineer

Kotlin Jetpack Compose 状态栏设置笔记

本文记录如何在 Kotlin Jetpack Compose 中设置状态栏颜色,同时区分了 API 21–API 34 与 API 35+ 的差异。


1. 使用 window.statusBarColor (API 21 - API 34)

  • API 历史:

    • Added in API level 21
    • Deprecated in API level 35
  • 方法签名:

    public abstract void setStatusBarColor(int color)
  • 使用方法:

val context = LocalContext.current
val window = (context as Activity).window
window.statusBarColor = Color.Blue.toArgb()

2. API 35 及以上官方推荐

  • 官方描述说明:
    在 API 35 及以后的版本中,该方法已经废弃,官方建议不再直接使用 window.statusBarColor,而是通过为 WindowInsets.Type.statusBars() 绘制合适的背景来实现类似效果。

3. 使用自定义绘制设置状态栏背景(Compose 示例)

在 Compose 中,可以结合 LocalDensityWindowInsets 来自定义绘制状态栏背景:

val density = LocalDensity.current
val statusBarHeight = WindowInsets.statusBars.getTop(density)

Box(
modifier = Modifier
.fillMaxWidth()
.height(with(density) { statusBarHeight.toDp() })
.background(Color(0xFF6200EA))
)

4. 使用WindowInsetsControllerCompat设置状态栏亮色暗色模式

val insetsControllerCompat = WindowInsetsControllerCompat(
window,
window.decorView
)
insetsControllerCompat.apply {
isAppearanceLightStatusBars = false / true
isAppearanceLightNavigationBars = false / true
}

Android 环境变量配置

· 阅读需 1 分钟
chihaicheng
Front End Engineer

📁 核心环境变量配置

环境变量名称路径示例作用说明
ANDROID_SDK_HOMED:\software\Android\.android控制模拟器(AVD)和SDK配置文件存储位置
GRADLE_USER_HOMED:\software\repository\.gradleGradle构建缓存和依赖库存储目录
KONAN_DATA_DIRD:\software\repository\.konanKotlin Native依赖缓存路径
MAVEN_HOMED:\software\JetBrains\IDEA\plugins\maven\lib\maven3Maven安装根目录
JAVACPP_CACHE_DIRD:\software\repository\.javacppJavaCPP本地库缓存目录