Thymeleaf 编辑表单详解:从字段回填到模拟 PUT 请求

文章来源声明: 原文作者:步行cgn; 来源站点:掘金; 原文链接:https://juejin.cn/post/7700798335888736271; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

文章把编辑表单的回填、主键提交和模拟PUT讲得很清楚,并点出th:field混用这一高频坑,适合Spring MVC与Thymeleaf开发者排错和规范写法参考。

Thymeleaf 编辑表单详解:从字段回填到模拟 PUT 请求 --------------------------------

原始代码

<form th:action="@{/user}" th:object="${user}" method="post">
    <input type="hidden" name=_method value="put"/>
    <input type="hidden" name="id" th:value="*{id}"/>
    <label>用户名:</label>
    <input type="text" name="username" th:value="*{username}" required>
    <label>性别:</label>
    <select name="gender" th:field="*{gender}" required>
        <option value="">-- 请选择 --</option>
        <option value="1" selected>男</option>
        <option value="0">女</option>
    </select>

    <label>邮箱:</label>
    <input type="email" name="email" th:value="*{email}" required>

    <button type="submit">修改</button>
</form>

一、这段代码的作用

这是一个“修改用户信息”的表单。它完成四件事:

  1. 打开页面时,从后端传入的 user 对象中读取数据,回填到输入框和下拉框。
  2. 提交时携带用户主键 id,让后端知道要修改哪一条记录。
  3. 通过隐藏字段 _method=put,把浏览器原生的 POST 请求模拟成 PUT 请求。
  4. 用户修改后提交,后端按 PUT 方法接收并更新数据。

整体数据流:

GET 打开编辑页
  → Controller 查询 User,放入 Model
  → Thymeleaf 用 th:object 绑定 user,用 *{} / th:field 回填
  → 用户修改并提交
POST /user
  → 携带 _method=put、id、username、gender、email
  → HiddenHttpMethodFilter 把 POST 改写为 PUT
  → Spring MVC 匹配 @PutMapping("/user")
  → 参数绑定到 User 对象,执行更新

二、逐行代码解释

1. form 标签

<form th:action="@{/user}" th:object="${user}" method="post">

属性作用
`th:action="@{/user}"`表单提交地址。`@{}` 是 URL 表达式,会自动加上项目上下文路径,例如项目部署在 `/usermgt` 下,最终生成 `/usermgt/user`。
`th:object="${user}"`把当前表单绑定到 Model 中的 `user` 对象。绑定后,表单内部可以用 `*{属性名}` 直接访问该对象的属性。
`method="post"`浏览器表单原生只支持 GET 和 POST,因此这里写 POST,真正的 PUT 由后面的 `_method` 隐藏字段模拟。

th:object 是整段代码的上下文开关。没有它,下面的 *{id}、*{username} 等表达式找不到目标对象,会直接报错。

2. 模拟 PUT 的隐藏字段

<input type="hidden" name=_method value="put"/>

浏览器表单不能直接发送 PUT 或 DELETE 请求。Spring MVC 提供的标准做法是:

  1. 表单以 POST 方式提交。
  2. 增加隐藏字段 _method,值为目标 HTTP 方法名。
  3. 服务端配置 HiddenHttpMethodFilter,它会读取 _method 参数,把请求方法从 POST 改写成 PUT 或 DELETE。

因此这一行的作用是:让后端把这次 POST 识别为 PUT 请求,这样 @PutMapping("/user") 才能匹配到。

当前写法 name=_method 没有加引号。HTML5 的宽松解析通常能运行,但规范写法应加引号:

<input type="hidden" name="_method" value="put"/>

3. 主键隐藏字段

<input type="hidden" name="id" th:value="*{id}"/>

修改操作必须提交主键,否则后端不知道要更新哪一条记录。

属性作用
`name="id"`提交时的参数名,后端用 `id` 接收。
`th:value="*{id}"`从 `th:object` 绑定的 `user` 对象中读取 `id`,渲染到 `value` 属性中。

这里的写法可以工作,但更简洁的方式是使用 th:field:

<input type="hidden" th:field="*{id}"/>

th:field 会自动生成 name="id"、id="id",并根据模型值生成 value,三件事一次完成。

4. 用户名输入框

<label>用户名:</label>
<input type="text" name="username" th:value="*{username}" required>

属性作用
`name="username"`提交参数名,Spring MVC 会把它绑定到 `User.username`。
`th:value="*{username}"`从 `user` 对象中读取用户名,回填到输入框。
`required`HTML5 原生校验,为空时浏览器阻止提交,不进入后端。

同样可以简化为:

<input type="text" th:field="*{username}" required>

th:field 会自动处理 name、id 和 value。

5. 性别下拉框(存在冲突)

<select name="gender" th:field="*{gender}" required>
    <option value="">-- 请选择 --</option>
    <option value="1" selected>男</option>
    <option value="0">女</option>
</select>

这一段存在一个典型问题:name、th:field 和手写 selected 混用,导致回填逻辑冲突。

写法实际作用
`name="gender"`手动指定提交参数名。
`th:field="*{gender}"`自动生成 `name="gender"` 和 `id="gender"`,并根据 `user.gender` 的值自动给对应 `option` 加上 `selected`。
`手写死“男”为选中状态。

问题在于:th:field 会自动处理选中逻辑,手写的 selected 会覆盖它的判断。结果是:不管 user.gender 是 0 还是 1,页面永远选中“男”,编辑回填失效。

正确写法二选一。

方案一:只用 th:field,去掉手写 selected 和手动 name

<select th:field="*{gender}" required>
    <option value="">-- 请选择 --</option>
    <option value="1">男</option>
    <option value="0">女</option>
</select>

方案二:不用 th:field,手动判断选中

<select name="gender" required>
    <option value="">-- 请选择 --</option>
    <option value="1" th:selected="${user.gender == 1}">男</option>
    <option value="0" th:selected="${user.gender == 0}">女</option>
</select>

两种方案都能工作,但不能混用。混用就是当前这段代码的问题。

6. 邮箱输入框

<label>邮箱:</label>
<input type="email" name="email" th:value="*{email}" required>

属性作用
`type="email"`HTML5 输入类型,浏览器会校验邮箱格式。
`name="email"`提交参数名。
`th:value="*{email}"`回填已有邮箱。
`required`必填校验。

可简化为:

<input type="email" th:field="*{email}" required>

7. 提交按钮

<button type="submit">修改</button>

type="submit" 触发表单提交。如果不写 type,<button> 默认也是 submit,但显式写出更清晰。

三、三种 Thymeleaf 表达式

表达式含义典型用途
`${...}`从 Model、request、session 等作用域取全局值`th:object="${user}"`
`*{...}`从 `th:object` 绑定的当前表单对象中取属性`th:value="*{username}"`
`@{...}`生成 URL,自动处理上下文路径`th:action="@{/user}"`

记忆口诀:

${} 找全局,
*{} 找当前表单对象,
@{} 生成 URL。

四、th:field 为什么能一个顶三个

th:field 会自动完成三件事:

  1. 生成 name 属性,提交时后端能收到参数。
  2. 生成 id 属性,方便 <label for> 关联。
  3. 读取模型值,自动回填输入框,或自动选中下拉框对应项。

因此使用 th:field 时,不要再手写 name 和 selected。混用会导致冲突,尤其是下拉框的选中状态。

记忆口诀:

th:field 一个顶三个:
生成 name、生成 id、自动回填。
用了 field,就别再手写 name 和 selected。

五、修正后的完整表单

<form th:action="@{/user}" th:object="${user}" method="post">
    <input type="hidden" name="_method" value="put"/>
    <input type="hidden" th:field="*{id}"/>

    <label>用户名:</label>
    <input type="text" th:field="*{username}" required>

    <label>性别:</label>
    <select th:field="*{gender}" required>
        <option value="">-- 请选择 --</option>
        <option value="1">男</option>
        <option value="0">女</option>
    </select>

    <label>邮箱:</label>
    <input type="email" th:field="*{email}" required>

    <button type="submit">修改</button>
</form>

改动点:

  1. name=_method 改为 name="_method",补上引号。
  2. 所有输入框统一使用 th:field,去掉手动的 name 和 th:value。
  3. 下拉框去掉手写的 selected,让 th:field 统一处理选中逻辑。

六、编辑表单的关键点

1. 必须携带主键

<input type="hidden" th:field="*{id}"/>

2. 模拟 PUT/DELETE 三步

第一步:form 的 method 写 post
第二步:增加隐藏字段 _method,值为 put 或 delete
第三步:服务端配置 HiddenHttpMethodFilter

Spring Boot 中可以通过配置开启:

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true

3. 前端校验只是辅助

required、type="email" 是浏览器层面的校验,用户可以绕过。后端仍然需要校验。

七、记忆方法

1. 表单四件套

th:action 定地址,
th:object 定对象,
method 定提交方式,
th:field 绑字段。

2. 编辑表单必带主键

修改先带 id,
隐藏字段传主键。

3. 模拟 PUT 三步

POST 提交,
_method 声明,
过滤器改写。

4. th:field 一个顶三个

生成 name,
生成 id,
自动回填。
用了 field,不写 name,不写 selected。

5. 三种表达式分工

${} 找全局,
*{} 找对象,
@{} 生成 URL。

6. 当前代码最大的坑

select 上同时出现 name、th:field、selected,
选中逻辑会打架,回填会失效。

八、总结

代码片段作用注意事项
`th:action="@{/user}"`提交地址,自动带上下文路径避免硬编码路径
`th:object="${user}"`绑定表单对象没有它 `*{}` 无法工作
`method="post"`浏览器原生提交方式PUT 靠 `_method` 模拟
`_method=put`让过滤器把 POST 改写为 PUT需要 `HiddenHttpMethodFilter`
`th:field="*{id}"`提交主键并回填编辑表单必须有
`th:field="*{username}"`生成 name/id 并回填不再手写 `name`
`th:field="*{gender}"`自动选中对应 option不再手写 `selected`
`required`浏览器必填校验后端仍需校验

这段代码的核心是 th:object、*{}、th:field 三者的配合。记住“对象绑定用 object,字段取值用星号,控件回填用 field”,再记住“编辑表单必带 id,模拟 PUT 加 _method”,这类表单就能独立写出。最容易出错的地方是 th:field 与手写 name、selected 混用,导致回填失效,修改时应统一交给 th:field 处理。