折腾了一下午Spring MVC 表单与控制器间model对象映射的问题,问题解决后,决定把这些经验分享给大家,有问题的地方,还请指教。
首先,这些问题都是基于你使用了spring的表单标签(标签用sf作为前缀),类如下面代码所示:
<sf:form method="post" commandName="account" >
<sf:errors path="*" element="div" cssClass="error" />
<sf:label path="loginName" cssErrorClass="error">账号: </sf:label><sf:input path="loginName" cssErrorClass="error"/><br/>
<sf:label path="name" cssErrorClass="error">姓名: </sf:label><sf:input path="name" cssErrorClass="error"/><br/>
<sf:label path="password" cssErrorClass="error">密码: </sf:label><sf:password path="password" cssErrorClass="error"/><br/>
<input type="button" value="注册" id="registerBtn"/>
</sf:form>
@RequestMapping(value="/register", method = RequestMethod.GET)
public String register(Model model) {
//model.addAttribute(new BaseAccount());
model.addAttribute("account", new BaseAccount());
return "a4/account/register";
}
通常你会通过GET方式请求这个页面,然后展示开头那个注册页面。这里一定要注意一下红色部分的代码:因为你的注册界面引用了spring的表单标签sf:form,且commanName属性指定了需要从上下文中取出一个key为account的model对象,所以,无论是哪个controller定向到这个视图,都必须返回一个key为account的model对象,这也是上面红色部分代码的意义。(同时,您还需要注意一下controller中model.addAttribute()方法,带key和不带key的区别,不带key,key就为被设置对象的simpleName,首字母小写,如上面的代码中,不设置key的话,spring默认设置的key就为baseAccount,这样返回去就会有问题)
如果你不按照上面的要求写代码,你将会得到如下类似的异常信息:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'account' available as request attribute
2、假设你的用户注册界面,用POST方式到了下面这个Controller中:
@RequestMapping(value="/register", method = RequestMethod.POST)
public String registerProcess(Model model, @ModelAttribute("account") @Valid BaseAccount account, Errors errors) {
if(errors.hasErrors()) {
return "a4/account/register";
}
long newId = this.accountService.registerAccount(account);
if(newId == -1) {
model.addAttribute("commonRegisterError", "账户创建失败");
return "a4/account/register";
}
model.addAttribute("accountId", account.getId());
return "redirect:/accounts/{accountId}";
}
我们先看第一个if分支,上面代码红色部分,意思是说表单验证失败的时候返回到注册界面。这里一定要注意了:结合第一点中所讲,我们这个地方并没有显示的设置key为account的model对象,因为我们任务request请求过来之后再次返回到那个注册界面,其中的model对象是不会丢的,所以我们不需要显示设置。这个逻辑是正确的,spring也支持,但需要注意的是这个地方有个陷阱,看看我们的注册页面表单的commanName和GET注册页面的model对象KEY都是account,如果你不按照上面代码的方法参数中用蓝色注解@ModelAttribute指定model的Key为account,你也为得到如下的异常信息:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'account' available as request attribute
另外:看看我们注册页面上的表单验证错误提示标签,如<sf:errors/>和<sf:label/>等,如果不按照第2点说的去做,即便后台表单验证有问题,也不会将错误信息显示到页面上的!问题的根源还是在这个Key上面。
最后,为了少出这种莫名其妙的问题,命名规范一致性确实很重要!上面那些代码,如果我们都用baseAccount作为key的话,就不会有那么多坑了。文末附上key都为baseAccount的代码:
<sf:form method="post" modelAttribute="baseAccount" >
<sf:errors path="*" element="div" cssClass="error" />
<c:if test="${!empty commonRegisterError}">
<span class="error">${commonRegisterError}</span><br/><br/>
</c:if>
<sf:label path="loginName" cssErrorClass="error">账号: </sf:label><sf:input path="loginName" cssErrorClass="error"/><br/>
<sf:label path="name" cssErrorClass="error">姓名: </sf:label><sf:input path="name" cssErrorClass="error"/><br/>
<sf:label path="password" cssErrorClass="error">密码: </sf:label><sf:password path="password" cssErrorClass="error"/><br/>
<br/>
<input type="button" value="注册" id="registerBtn"/>
</sf:form>
@RequestMapping(value="/register", method = RequestMethod.GET)
public String register(Model model) {
model.addAttribute(new BaseAccount());
return "a4/account/register";
}
@RequestMapping(value="/register", method = RequestMethod.POST)
public String registerProcess(Model model, @Valid BaseAccount baseAccount, Errors errors) {
if(errors.hasErrors()) {
return "a4/account/register";
}
long newId = this.accountService.registerAccount(baseAccount);
if(newId == -1) {
model.addAttribute("commonRegisterError", "账户创建失败");
return "a4/account/register";
}
model.addAttribute("accountId", baseAccount.getId());
return "redirect:/accounts/{accountId}";
}