spring roo 中自定义View时出现 Neither BindingResult nor plain target object for bean name ‘command’ available as request attribute 错误

问题描述

在做项目的时候,想为一些自定义页面的form添加下拉选项菜单,例如为一些finder添加搜索条件:

<form:find finderName="findSensorDatas" id="ff_demo_imlab_ims_entity_SensorData" path="/sensordatas" z="user-managed">
    ...
<field:select nullOption="true" field="state" id="c_demo_imlab_ims_entity_StateEnum_type" items="${states}" path="/state" required="false" z="user-managed"/>
    ...
</form:find>

其中 states 为枚举类型. roo自动生成的带有枚举类型参数的finder时, 不会为enum生成下拉菜单, 而是一个简单的input控件.

如果你想提升用户体验, 加入下拉菜单, 并且显示已有的枚举类型选项, 那么如果你直接将input标签改成select标签(就算你从create.jspx页面拷贝过来), 它会有如下提示:

Neither BindingResult nor plain target object for bean name 'command' available as request attribute

解决思路

  1. 在项目里全局搜索 command 字段, 没有找到.
  2. 打开 select.tagx 文件和 find.tagx*||| 中寻找线索, 无所获.
  3. 百度和google之, 无所获.
  4. 偶然发现设置 select 标签的属性 disableFromBinding=”false” 后, 居然不会提示错误了!
  5. 回过头来仔细研究 select.tagx 中关于 disableFromBinding 变量的逻辑. 发现跟spring binding有关.
  6. 百度和google “spring bind”, 有所获.
  7. 写blog来备忘

具体解决方案

什么是 spring binding ?

The spring:bind tag provides you with support for evaluation of the status of a certain bean or bean property. The status of a bean includes the value of the actual bean or bean property you’re evaluating as well as possibily available errors and the expression to use in forms in order for the databinding functionality to be able to bind the properties again when submitting for instance a form. [see this]

的”path”屬性設定了要綁定的表單物件名稱,這個名稱是設定在loginController中的 “commandName”屬性,預設名稱是”command”,當設定為”command.*”時,表示綁定表單物件上所有相關的數據, “status”的”errorMessage”會顯示在Controller中設定的錯誤訊息,這待會在Controller的實作中會再看到說明。在表單中,對於”username”欄位,綁定了”command.username”屬性,”status”的”expression”會顯示綁定的屬 性名稱,而”value”則顯示表單物件中所儲存的值,這邊設計的程式在登入失敗後會回到form.jsp,這樣可以在同一個頁面上顯示錯誤訊息與之前輸 入錯誤的值。[see this]

另外在stackoverflow上也找到类似的问题:

See this link for an explanation of what the status variables mean.

status.expression: the expression that was used to retrieve the bean or property status.value: the actual value of the bean or property (transformed using registered PropertyEditors) status.errorMessages: an array of error messages, resulting from validation The status object is evaluated when the binding is done.

Also have in mind that Spring 2.0 introduced new form tags, which are probable better suited for your needs.

简单地说, spring:bind提供了一种表单字段表单类属性绑定的机制. 所谓”绑定”, 简单地说就是将 ${status} 和 spring:bind 代表的属性关联起来了.

WTF, 这是什么意思?

如下所示:

<spring:bind path="company.name">
    ## render a form field, containing the value and the expression
    Name: <input 
        type="text" 
        value="<c:out value="${status.value}"/>"
        name="<c:out value="${status.expression}"/>">
        ## if there are error codes, display them!
        <c:if test="${status.error}">
            Error codes:
            <c:forEach items="${status.errorMessages}" var"error">
                <c:out value="${error}"/>
            </c:forEach>
        </c:if>
</spring:bind>

直观地感受上面的html代码, 可以发现<spring:bind>提供了一种类属性的访问机制, 令到<input>的value属性和name属性可以由company来赋值. <input>的值也会被赋予到company的那么属性上.

那么 “${status.errorMessages}” 是什么东西?

如果说status和company绑定了, 那么errorMessage属性是从哪里来的? 查阅spring的文档, 看到Validation, Data Binding, and Type Conversion一章, 以spring:bind为关键字搜索, 看到如下一些描述

Let’s consider a small data object:

public class Person {
      private String name;
    private int age;
    // the usual getters and setters...
}

Implementing a Validator is fairly straightforward, especially when you know of the ValidationUtils helper class that the Spring Framework also provides.

public class PersonValidator implements Validator {

/**
* This Validator validates *just* Person instances
*/
public boolean supports(Class clazz) {
    return Person.class.equals(clazz);
}

public void validate(Object obj, Errors e) {
    ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
    Person p = (Person) obj;
    if (p.getAge() < 0) {
        e.rejectValue("age", "negativevalue");
    } else if (p.getAge() > 110) {
        e.rejectValue("age", "too.darn.old");
    }
}
}

…Validation errors are reported to the Errors object passed to the validator. In case of Spring Web MVC you can use <spring:bind/> tag to inspect the error messages, but of course you can also inspect the errors object yourself. More information about the methods it offers can be found from the Javadoc.

注意到“In case of Spring Web MVC you can use <spring:bind/> tag to inspect the error messages”一句, 可以知道, ${status}访问的不是company本身, 而是company的Validator(?).

也就是说, <spring:bind/>提供了访问绑定的类的验证信息(Validator)的能力.

具体流程

google了一下spring MVC,发现了这样一篇博客:

在Spring Web MVC(3.0之前)环境中,数据类型转换、验证及格式化通常是这样使用的:

流程

留意到其中的WebDataBinderPropertyEditor,它们起到了一个类型转换的作用。

WebDataBinder

再google了一下,找到了这里

public class WebDataBinder
extends DataBinder

Special DataBinder for data binding from web request parameters to JavaBean objects. Designed for web environments, but not dependent on the Servlet API; serves as base class for more specific DataBinder variants, such as ServletRequestDataBinder.

Includes support for field markers which address a common problem with HTML checkboxes and select options: detecting that a field was part of the form, but did not generate a request parameter because it was empty. A field marker allows to detect that state and reset the corresponding bean property accordingly.

可以看出WebDataBinder继承于DataBinder,专为Web环境而设置,为表单提交提供了支持。

再看一下DataBinder:

Binder that allows for setting property values onto a target object

, including support for validation and binding result analysis. The binding process can be customized through specifying allowed fields, required fields, custom editors, etc.

看来实现从表单到表单object转换的核心就是这个东西了。

PropertyEditor

PropertyEditor的描述在这里

A PropertyEditor class provides support for GUIs that want to allow users to edit a property value of a given type.

但注意,PropertyEditor 仅能实现从String到Object的转换(setAsText(String text)和getAsText())方法。

这里也提到:

一般地,我们要使用PropertyEditor时,并不直接实现此接口,而是通过继承实现此接口的java.beans.PropertyEditorSupport来简化我们的工作,在子类覆盖setAsText方法就可以了

但是在spring mvc 3.0以后,数据类型转换、验证和格式化的框架通常如下图所示:

框架

其中WebDataBinder通过ConverterSPI和FormatterSPI实现了从任意类型到任意类型的转换。关于这个另外博文进行交流。

综上

可以得知,声明令表单数据通过DataBinder转化成FormObject,然后由Validator来对其进行验证,若出错,则写Error到FormObject的error属性并返回view层,否则正常往下执行。

回到问题本身

注意Neither BindingResult nor plain target object for bean name ‘command’ available as request attribute这句话, 大意是在request中找不到command属性. command是从哪里来的呢?

查了一下资料, 如下所示:

3-2 commandName
用来指定JSP中的数据需要绑定到哪个对象。默认为command
比如下面的配置中,commandName就是command
 <spring:bind path='command.email'>
 <td><input type='text' name='${status.expression}' 
    value='${status.value}' size='30' 
    maxlength='100'></td></tr>
 </spring:bind>

因为是缺省值,所以它就不需要再在Controller中显示声明

如上所述, command是默认的绑定名字. 那么为什么create.jspx就可以有枚举类型的select, 而finder.tagx就没有呢?

猜测finder.tagx是不是默认没有绑定对象?打开controller的*.aj文件, 观察到如下所示代码:

@RequestMapping(params = "form", produces = "text/html")
    public String SensorController.createForm(Model uiModel) {
        populateEditForm(uiModel, new Sensor());
        List<String[]> dependencies = new ArrayList<String[]>();
        if (SensorType.countSensorTypes() == 0) {
            dependencies.add(new String[] { "sensortype", "sensortypes" });
        }
        uiModel.addAttribute("dependencies", dependencies);
        return "sensors/create";
    }
...
void SensorController.populateEditForm(Model uiModel, Sensor sensor) {
        uiModel.addAttribute("sensor", sensor);
        uiModel.addAttribute("sensordatas", autoRefreshService.findAllSensorDatas());
        uiModel.addAttribute("sensortypes", SensorType.findAllSensorTypes());
        uiModel.addAttribute("sensorlayouts", layoutService.findAllSensorLayouts());
}

留意到populateEditForm方法在Model中放置了一个sensor对象, 这就是解决问题的所在?将该行注释掉:

//uiModel.addAttribute("sensor", sensor);

访问create.jspx页面, sts控制台出现了 Neither BindingResult nor plain target object for bean name ‘sensor’ available as request attribute 一行红字. 可以看出, 如果使用了spring的binding技术, 就必须提供绑定类, 否则会出现上述错误.

打开create.tagx页面, 注意到如下代码:

<jsp:directive.attribute name="modelAttribute" type="java.lang.String" required="true" rtexprvalue="true" description="The name of the model attribute for form binding"/>
...
<form:form action="${form_url}" method="POST" modelAttribute="${modelAttribute}" enctype="${enctype}" onsubmit="${jsCall}">
...
</form:form>

可以看出, **在显示create.jspx页面时, 标签指定了要绑定的类的名字, 也就是上文中的 modelAttribute. 另外, 这里也说到:

In above JSP file, we display contact details in a table. Also each attribute is displayed in a textbox. Note that modelAttribute=”contactForm” is defined in tag. This tag defines the modelAttribute name for Spring mapping. On form submission, Spring will parse the values from request and fill the ContactForm bean and pass it to the controller.

大致可以形成这样一种印象: **spring mvc中, 表单应该是一个类, 包装了从controller到view层传递的数据. 表单中的字段对应这个类的属性.***

改造select.tagx?

打开select.tagx 注意到下面所示的一行:

这不就是解决方案吗?马上将

<field:select nullOption="true" field="state" id="c_demo_imlab_ims_entity_StateEnum_type" items="${states}" path="/state" required="false" z="user-managed"/>

改成

<field:nullableselect nullOption="true" disableFormBinding="true" field="state" id="c_demo_imlab_ims_entity_StateEnum_type" items="${states}" path="/state" required="false" z="user-managed"/>

程序通过了!

发生了什么?

select.tagx中有如下代码:

<c:when test="${disableFormBinding}">
...
    <select id="_${sec_field}_id" name="${sec_field}" multiple="${multiple}">
        <c:forEach items="${items}" var="item">
        <option value="${item}">
...
<c:otherwise>
...
    <form:select id="_${sec_field}_id" items="${items}" path="${sec_field}" disabled="${disabled}" multiple="${multiple}" />
...
    <br />
    <form:errors cssClass="errors" id="_${sec_field}_error_id" path="${sec_field}" />
</c:otherwise>

${disableFormBinding}为真时, 采用不带binding的<select><option>标签, 否则采用spring的<form:select>标签, 而path=”${sec_field}”就是绑定的属性名


后记

虽然被这个问题困扰了很久, 最终解决方案也只是修改了一个标签属性而已, 但是其中学到的东西远不止这些!