Thymeleaf 编辑表单详解:从字段回填到模拟 PUT 请求
原始代码
html
<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>
一、这段代码的作用
这是一个"修改用户信息"的表单。它完成四件事:
- 打开页面时,从后端传入的
user对象中读取数据,回填到输入框和下拉框。 - 提交时携带用户主键
id,让后端知道要修改哪一条记录。 - 通过隐藏字段
_method=put,把浏览器原生的 POST 请求模拟成 PUT 请求。 - 用户修改后提交,后端按 PUT 方法接收并更新数据。
整体数据流:
sql
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 标签
html
<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 的隐藏字段
html
<input type="hidden" name=_method value="put"/>
浏览器表单不能直接发送 PUT 或 DELETE 请求。Spring MVC 提供的标准做法是:
- 表单以 POST 方式提交。
- 增加隐藏字段
_method,值为目标 HTTP 方法名。 - 服务端配置
HiddenHttpMethodFilter,它会读取_method参数,把请求方法从 POST 改写成 PUT 或 DELETE。
因此这一行的作用是:让后端把这次 POST 识别为 PUT 请求 ,这样 @PutMapping("/user") 才能匹配到。
当前写法 name=_method 没有加引号。HTML5 的宽松解析通常能运行,但规范写法应加引号:
html
<input type="hidden" name="_method" value="put"/>
3. 主键隐藏字段
html
<input type="hidden" name="id" th:value="*{id}"/>
修改操作必须提交主键,否则后端不知道要更新哪一条记录。
| 属性 | 作用 |
|---|---|
name="id" |
提交时的参数名,后端用 id 接收。 |
th:value="*{id}" |
从 th:object 绑定的 user 对象中读取 id,渲染到 value 属性中。 |
这里的写法可以工作,但更简洁的方式是使用 th:field:
html
<input type="hidden" th:field="*{id}"/>
th:field 会自动生成 name="id"、id="id",并根据模型值生成 value,三件事一次完成。
4. 用户名输入框
html
<label>用户名:</label>
<input type="text" name="username" th:value="*{username}" required>
| 属性 | 作用 |
|---|---|
name="username" |
提交参数名,Spring MVC 会把它绑定到 User.username。 |
th:value="*{username}" |
从 user 对象中读取用户名,回填到输入框。 |
required |
HTML5 原生校验,为空时浏览器阻止提交,不进入后端。 |
同样可以简化为:
html
<input type="text" th:field="*{username}" required>
th:field 会自动处理 name、id 和 value。
5. 性别下拉框(存在冲突)
html
<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。 |
<option value="1" selected> |
手写死"男"为选中状态。 |
问题在于:th:field 会自动处理选中逻辑,手写的 selected 会覆盖它的判断。结果是:不管 user.gender 是 0 还是 1,页面永远选中"男",编辑回填失效。
正确写法二选一。
方案一:只用 th:field,去掉手写 selected 和手动 name
html
<select th:field="*{gender}" required>
<option value="">-- 请选择 --</option>
<option value="1">男</option>
<option value="0">女</option>
</select>
方案二:不用 th:field,手动判断选中
html
<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. 邮箱输入框
html
<label>邮箱:</label>
<input type="email" name="email" th:value="*{email}" required>
| 属性 | 作用 |
|---|---|
type="email" |
HTML5 输入类型,浏览器会校验邮箱格式。 |
name="email" |
提交参数名。 |
th:value="*{email}" |
回填已有邮箱。 |
required |
必填校验。 |
可简化为:
html
<input type="email" th:field="*{email}" required>
7. 提交按钮
html
<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}" |
记忆口诀:
bash
${} 找全局,
*{} 找当前表单对象,
@{} 生成 URL。
四、th:field 为什么能一个顶三个
th:field 会自动完成三件事:
- 生成
name属性,提交时后端能收到参数。 - 生成
id属性,方便<label for>关联。 - 读取模型值,自动回填输入框,或自动选中下拉框对应项。
因此使用 th:field 时,不要再手写 name 和 selected。混用会导致冲突,尤其是下拉框的选中状态。
记忆口诀:
bash
th:field 一个顶三个:
生成 name、生成 id、自动回填。
用了 field,就别再手写 name 和 selected。
五、修正后的完整表单
html
<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>
改动点:
name=_method改为name="_method",补上引号。- 所有输入框统一使用
th:field,去掉手动的name和th:value。 - 下拉框去掉手写的
selected,让th:field统一处理选中逻辑。
六、编辑表单的关键点
1. 必须携带主键
html
<input type="hidden" th:field="*{id}"/>
2. 模拟 PUT/DELETE 三步
arduino
第一步:form 的 method 写 post
第二步:增加隐藏字段 _method,值为 put 或 delete
第三步:服务端配置 HiddenHttpMethodFilter
Spring Boot 中可以通过配置开启:
yaml
spring:
mvc:
hiddenmethod:
filter:
enabled: true
3. 前端校验只是辅助
required、type="email" 是浏览器层面的校验,用户可以绕过。后端仍然需要校验。
七、记忆方法
1. 表单四件套
vbnet
th:action 定地址,
th:object 定对象,
method 定提交方式,
th:field 绑字段。
2. 编辑表单必带主键
bash
修改先带 id,
隐藏字段传主键。
3. 模拟 PUT 三步
POST 提交,
_method 声明,
过滤器改写。
4. th:field 一个顶三个
bash
生成 name,
生成 id,
自动回填。
用了 field,不写 name,不写 selected。
5. 三种表达式分工
bash
${} 找全局,
*{} 找对象,
@{} 生成 URL。
6. 当前代码最大的坑
csharp
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 处理。