Debugging Spring MVC collection binding

2019-07-28 18:41发布

I have a pretty simple user settings form:

<form:form method="post" id="fm1" cssClass="fm-v clearfix" commandName="${commandName}" htmlEscape="true">
  <div class="row fl-controls-left">
    <spring:message code="screen.userSettings.label.timeZone.accesskey" var="timeZoneAccessKey" />
    <label for="timeZone" class="fl-label"><spring:message code="screen.userSettings.label.timeZone" /></label>
    <form:select id="timeZone" path="timeZone" accesskey="${timeZoneAccessKey}">
      <form:options items="${user.supportedTimeZones}" itemLabel="label" itemValue="id" />
    </form:select>

    <c:forEach items="${user.answers}" var="answer" varStatus="loop">
      <div class="row fl-controls-left">
        <form:select path="answers[${loop.index}].questionId">
          <option value="-1"><spring:message code="screen.userSettings.question.selectOne" /></option>
          <form:options items="${user.supportedQuestions}" itemLabel="question" itemValue="id" />
        </form:select>
        <form:input path="answers[${loop.index}].answer" size="30" autocomplete="false" htmlEscape="true" type="text" cssClass="required" cssErrorClass="error"/>
      </div>
    </c:forEach>
  </div>
</form:form>

Basically, you can set your timezone, and provide answers to a list forgotten password questions. The model object bound to this form is basically this:

public class User implements Serializable {
    private static final long serialVersionUID = 8974875234954842283L;

    private List<Answer> answers;
    private List<Question> supportedQuestions;
    private List<TimeZone> supportedTimeZones;
    private String timeZone;

    public User() {
        credentials = new UsernamePasswordCredentials();
    }

    public List<Answer> getAnswers() {
        return answers;
    }

    public List<Question> getSupportedQuestions() {
        return supportedQuestions;
    }

    public List<TimeZone> getSupportedTimeZones() {
        return supportedTimeZones;
    }

    public String getTimeZone() {
        return timeZone;
    }

    public void setAnswers( List<Answer> answers ) {
        this.answers = answers;
    }

    public void setSupportedQuestions( List<Question> supportedQuestions ) {
        this.supportedQuestions = supportedQuestions;
    }

    public void setSupportedTimeZones( List<TimeZone> supportedTimeZones ) {
        this.supportedTimeZones = supportedTimeZones;
    }

    public void setTimeZone( String timeZone ) {
        this.timeZone = timeZone;
    }

    ...
}

The form displays just fine. It provides a single drop down showing all supported timezones followed by few rows of question/answer pairs with the questions being drop downs containing all of the supported questions. When I submit the form back, processing fails because the List<Answer> answers is not getting updated with the values from the form. My question is two fold, first, what did I do wrong? Second, how should I approach debugging this type of mapping problem in the future?

I have tried stepping through the code with the debugger. The setTimeZone() method is getting called with the value from the form, but the setAnswers() method is not. Nor are the setQuestionId() or setAnswer() methods of the Answer class. I looked at the Request object and it has keys for all the answer[x].questionId and answer[x].answer parameters, but not sure what the values are. What would you recommend doing to track down this type of issue?

-------------------------- UPDATE --------------------------------

I just pulled this from the debugger:

map[
'answers[4].questionId' -> 'cn=honeymoon,ou=questions,dc=company'
'answers[2].questionId' -> 'cn=honeymoon,ou=questions,dc=company'
'answers[0].questionId' -> 'cn=firstPet,ou=questions,dc=company'
'lt' -> 'LT-786e9b77-efbd-f302-062e-90364cc4634aZe1s2'
'answers[1].answer' -> 'qwer'
'answers[3].answer' -> 'poiu'
'submit' -> 'Save'
'answers[3].questionId' -> 'cn=honeymoon,ou=questions,dc=company'
'answers[1].questionId' -> 'cn=mothersMaiden,ou=questions,dc=company'
'answers[2].answer' -> 'zxcv'
'answers[4].answer' -> 'lkjh'
'timeZone' -> 'America/New_York'
'_eventId' -> 'submit'
'answers[0].answer' -> 'asdf'
]

It is what is contained in the requestParameterMap. Clearly it shows that values were returned for answers (both questionId and answer). Why is spring not calling setAnswers() on the model object (remember that setTimeZone() is getting called)?

1条回答
戒情不戒烟
2楼-- · 2019-07-28 19:17

This turns out to be an issue with the binder in the viewState. It seems like this should work:

<binder>
  <binding property="answers" />
  <binding property="timeZone" />
</binder>

because answers is the property of the bound model object. However, I actually have to specify the attributes of the elements within the list:

<binder>
  <binding property="answers[0].questionId" />
  <binding property="answers[0].answer" />
  <binding property="answers[1].questionId" />
  <binding property="answers[1].answer" />
  <binding property="answers[2].questionId" />
  <binding property="answers[2].answer" />
  <binding property="answers[3].questionId" />
  <binding property="answers[3].answer" />
  <binding property="answers[4].questionId" />
  <binding property="answers[4].answer" />
  <binding property="timeZone" />
</binder>

For me this will work (for now) as I have a fixed size number of elements. Its a little painful to have to include every property of every element in the list, but it works. There are bugs/feature requests filed for this that should make it easier, but until then...

查看更多
登录 后发表回答