You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpringBoot+Thymeleaf(Kotlin)更新User对象时部分字段丢失问题

Kotlin + Spring Boot + Thymeleaf 用户更新时字段丢失问题解决

问题场景与现象

我用Kotlin结合Spring Boot和Thymeleaf实现用户更新功能,执行流程如下:

  1. 查询用户数据并传递到更新页面:
@GetMapping("/update/{id}")
fun updateUser(@PathVariable(value = "id") id: Int, model: Model): String {
    val user = userService.findUserByUid(id)
    model.addAttribute("user", user)
    model.addAttribute("allRoles", if (user.uid != 1) userService.addUserRole() else userService.adminRole())
    return "updateUser"
}
  1. 前端HTML表单修改用户部分属性:
<form action="#" th:action="@{/userManagement/update}" th:object="${user}" method="POST">
    <label>昵称</label>
    <input type="text" th:field="*{usernickname}" placeholder="usernickname" class="form-control mb-4 col-4">
    <br>
    <label>密码</label>
    <input type="password" th:field="*{password}" placeholder="password" class="form-control mb-4 col-4">
    <div>
        <label>职位:
            <input type="radio" name="roles"
                   th:each="myRole : ${allRoles}"
                   th:text="${myRole.name()}"
                   th:value="${myRole}"
                   th:field="*{usertype}"
                   th:attr="checked=${myRole.ordinal()==0?true:false}"
            />
        </label>
    </div>
    <br>
    <button type="submit" class="btn btn-update">更新员工</button>
</form>
  1. 后端接收表单数据并更新用户:
@PostMapping(path = ["/update"])
fun updateEmployee(@ModelAttribute("user") user: User, model: Model): String {
    try {
        user.password = passwordConfig.passwordEncoder().encode(user.password)
        userService.updateUser(user)  
    } catch (ex: RuntimeException) {
        model.addAttribute("error", ex.message)
        model.addAttribute("allRoles", if (user.uid != 1) userService.addUserRole() else userService.adminRole())
        return "updateUser"
    }
    return "redirect:/userManagement/"
}

提交表单后出现异常:后端接收的User对象仅包含usernickname、password、usertype字段,其他字段(如uid、username、usergroup)均为默认值或null。例如传递到页面的用户是{uid=5,username=aaa,usernickname=bbb,password=****,usertype=MANAGER,usergroup=DEFAULT,bugList={}},提交后变成{uid=0,username=null,usernickname=ccc,password=*****,usertype=MANAGER,usergroup=null,bugList=null}。

关联代码补充:
User实体类:

@Entity
class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    var uid = 0

    lateinit var password: String
    lateinit var username: String
    lateinit var usernickname: String
    lateinit var usertype: UserRole

    @ManyToOne(fetch= FetchType.EAGER, optional = true)
    @JoinColumn(name="groupid")
    lateinit var usergroup: UserGroup

    @OneToMany(mappedBy = "bid")
    lateinit var bugList: List<Bug>

    constructor(password: String, username: String, usernickname: String, usertype: UserRole, usergroup: UserGroup) {
        this.password = password
        this.username = username
        this.usernickname = usernickname
        this.usertype = usertype
        this.usergroup = usergroup
    }

    constructor()
}

UserRole枚举类:

enum class UserRole {
    ADMIN, PROGRAMMER, TESTER, MANAGER;

    val roleAuthority: GrantedAuthority
        get() = SimpleGrantedAuthority("ROLE_$name")
}

问题原因

HTTP表单提交只会发送表单中明确声明的字段,Spring MVC通过@ModelAttribute绑定对象时,会调用无参构造创建新的User实例,仅对表单提交的字段赋值。其他未提交的字段要么保持默认值(如uid的初始值0),要么因为lateinit修饰的字段未被赋值,通过反射机制被设置为null(绕过了Kotlin的lateinit非空检查),最终导致接收的User对象缺失大量原有数据。

解决方案

方案一:表单添加隐藏字段传递必要数据

在HTML表单中添加隐藏输入框,将需要保留的用户字段(如uid、username,以及关联对象的主键等)传递回后端:

<form action="#" th:action="@{/userManagement/update}" th:object="${user}" method="POST">
    <!-- 隐藏字段传递uid和username -->
    <input type="hidden" th:field="*{uid}" />
    <input type="hidden" th:field="*{username}" />
    <!-- 传递usergroup的主键,保证关联关系不丢失 -->
    <input type="hidden" th:field="*{usergroup.groupid}" />

    <label>昵称</label>
    <input type="text" th:field="*{usernickname}" placeholder="usernickname" class="form-control mb-4 col-4">
    <br>
    <label>密码</label>
    <input type="password" th:field="*{password}" placeholder="password" class="form-control mb-4 col-4">
    <div>
        <label>职位:
            <input type="radio" name="roles"
                   th:each="myRole : ${allRoles}"
                   th:text="${myRole.name()}"
                   th:value="${myRole}"
                   th:field="*{usertype}"
                   th:attr="checked=${myRole.ordinal()==0?true:false}"
            />
        </label>
    </div>
    <br>
    <button type="submit" class="btn btn-update">更新员工</button>
</form>

表单提交时会同步发送这些隐藏字段的值,Spring MVC就能完整绑定所有必要字段,保证User对象的完整性。

方案二:后端查询原有用户并覆盖更新字段(推荐)

这种方式更安全,避免前端篡改敏感字段(如username),后端主动从数据库获取原有用户数据,仅更新允许修改的字段:

@PostMapping(path = ["/update"])
fun updateEmployee(@ModelAttribute("user") submittedUser: User, model: Model): String {
    try {
        // 从数据库获取原始用户数据
        val existingUser = userService.findUserByUid(submittedUser.uid)
        // 更新允许修改的字段
        existingUser.usernickname = submittedUser.usernickname
        // 仅当用户输入了新密码时才更新,避免空密码覆盖原有密码
        if (submittedUser.password.isNotBlank()) {
            existingUser.password = passwordConfig.passwordEncoder().encode(submittedUser.password)
        }
        existingUser.usertype = submittedUser.usertype
        // 其他字段保持数据库中的原始值
        userService.updateUser(existingUser)  
    } catch (ex: RuntimeException) {
        model.addAttribute("error", ex.message)
        // 重新查询用户数据回显到页面
        val user = userService.findUserByUid(submittedUser.uid)
        model.addAttribute("user", user)
        model.addAttribute("allRoles", if (user.uid != 1) userService.addUserRole() else userService.adminRole())
        return "updateUser"
    }
    return "redirect:/userManagement/"
}

该方案无需前端传递所有字段,后端完全控制可更新的字段,既避免了字段丢失问题,也提升了系统安全性。

内容的提问来源于stack exchange,提问作者Draculea

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 12:55:19