记录下thymeleaf 3.0新增的th:fieldth:each中生成的变量结合使用时的坑。

代码就是官方例子https://github.com/thymeleaf/thymeleafexamples-stsm.git,在webapp/WEB-INF/seedstartermng.html中118-131行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<tr th:each="row,rowStat : *{rows}">
  <td th:text="${rowStat.count}">1</td>
  <td>
    <select th:field="*{rows[__${rowStat.index}__].variety}">
      <option th:each="var : ${allVarieties}" th:value="${var.id}" th:text="${var.name}">Thymus Thymi</option>
    </select>
  </td>
  <td>
    <input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}" th:errorclass="fieldError" />
  </td>
  <td>
    <button type="submit" name="removeRow" th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button>
  </td>
</tr>

其中第二个td取出rows中的每一行的variety,做成一个select下拉菜单,当我尝试用${row.variety}替换原文的*{rows[__${rowStat.index}__].variety}时,页面渲染抛出异常Neither BindingResult nor plain target object for bean name 'row' available as request attribute。多方探索之后得出结论:这是我们使用的问题,设计时特意在遇到这种情况时为了避免服务端无法处理提交后的表单,就抛出异常。

用例子中的代码渲染完生成后的页面为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<tr>
  <td>1</td>
  <td>
    <select id="rows0.variety" name="rows[0].variety">
      <option value="1">Thymus vulgaris</option>
      <option value="2">Thymus x citriodorus</option>
      <option value="3">Thymus herba-barona</option>
      <option value="4">Thymus pseudolaginosus</option>
      <option value="5">Thymus serpyllum</option>
    </select>
  </td>
  <td>
    <input type="text" id="rows0.seedsPerCell" name="rows[0].seedsPerCell" value="">
  </td>
  <td>
    <button type="submit" name="removeRow" value="0">Remove row</button>
  </td>
</tr>

其中,th:field在渲染时会给目标元素加上id和name属性,name属性取*{...}中表达式的值。由于__${rowStat.index}__是个预处理语句,在渲染之前会直接文本替换,所以最终的表达式为rows[0].variety

但是如果是${row.variety},则最终name会被替换为row.variety,由于浏览器作为客户端无法知道服务端的逻辑,这样之后的每个tr中的元素都是这个表达式,表单提交的时候无法被spring解析成一个list对象,就会出现错误。

官方站在实现角度的解释可以看这个issue