SpringBoot+Thymeleaf(Kotlin)更新User对象时部分字段丢失问题
问题场景与现象
我用Kotlin结合Spring Boot和Thymeleaf实现用户更新功能,执行流程如下:
- 查询用户数据并传递到更新页面:
@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" }
- 前端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>
- 后端接收表单数据并更新用户:
@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

