Spring3 MVC的最佳实践和理解(7)
?
个人学习参考所用,勿喷!?
?
7.表单处理和多页表单向导
7.1)表单处理。
表单处理很常见。比如我们有下面的表单:
<form:form method="post" modelAttribute="reservation"><form:errors path="*" css/><table> <tr> <td>Court Name</td> <td><form:input path="courtName" /></td> <td><form:errors path="courtName" css/></td> </tr> <tr> <td>Date</td> <td><form:input path="date" /></td> <td><form:errors path="date" css/></td> </tr> <tr> <td>Hour</td> <td><form:input path="hour" /></td> <td><form:errors path="hour" css/></td> </tr> <tr> <td>Player Name</td> <td><form:input path="player.name" /></td> <td><form:errors path="player.name" css/></td> </tr> <tr> <td>Player Phone</td> <td><form:input path="player.phone" /></td> <td><form:errors path="player.phone" css/></td> </tr> <tr> <td colspan="3"><input type="submit" /></td> </tr></table></form:form>
?
这里使用的是spring的标签,HTML中需要<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>。 很多属性一看就知道含义。需要注意的是,modelAttribute属,这里联想下Struts2中的值栈就能够明白这里代表的是处理的返回结果。
编写后台的控制器类:
@Controller// Bind controller to URL /reservationForm// initial view will be resolved to the name returned in the default GET method@RequestMapping("/reservationForm")// Add Reservation object to session, since its created on setup and used after submission@SessionAttributes("reservation") // Command name class was used in earlier Spring versionspublic class ReservationFormController { private ReservationService reservationService; // Wire service in constructor, available in application context @Autowired public ReservationFormController(ReservationService reservationService) { this.reservationService = reservationService; } // Create attribute for model // Will be represented as drop box Sport Types in reservationForm @ModelAttribute("sportTypes") public List<SportType> populateSportTypes() { return reservationService.getAllSportTypes(); } // Controller will always look for a default GET method to call first, irrespective of name // In this case, named setupForm to ease identification @RequestMapping(method = RequestMethod.GET) public String setupForm( @RequestParam(required = false, value = "username") String username,Model model) { Reservation reservation = new Reservation(); reservation.setPlayer(new Player(username, null)); model.addAttribute("reservation", reservation); return "reservationForm"; } // Controller will always look for a default POST method irrespective of name // when a submission ocurrs on the URL (i.e.@RequestMapping(/reservationForm)) // In this case, named submitForm to ease identification @RequestMapping(method = RequestMethod.POST) // Model reservation object, BindingResult and SessionStatus as parameters public String submitForm(@ModelAttribute("reservation") Reservation reservation, BindingResult result, SessionStatus status) { reservationService.make(reservation);return "redirect:reservationSuccess"; }}?需要注意的是@SessionAttributes("reservation")注解和submitForm(...)方法的返回值方式return "redirect:reservationSuccess"。
@SessionAttributes是的reservation对象唯一的存在于用户会话中,目的是为了保持用户每次提交的数据(用户提交可能会出错,所以不是一次提交就一定能够成功)。
"redirect:reservationSuccess"逻辑视图表明重定向到reservationSuccess逻辑视图,应用中设定是返回reservationSuccess.jsp,这样做的目的是为了防止提交成功后的刷新造成再次提交。这里应用了post/redirect/get设计模式来处理这个问题。
?
提供表单参考数据。例如这里的体育类型查考选择框:
<tr><td>Sport Type</td><td> <form:select path="sportType" items="${sportTypes}"itemValue="id" itemLabel="name" /></td><td><form:errors path="sportType" css/></td></tr>?
这里需要在控制器中添加一个@ModelAttribute("sportTypes") 标识的模式属性sportTypes(@ModelAttribute用于定义全局模式属性),在前面的submitForm(...)中也有@ModelAttribute("reservation") 用于应用session中的reservation对象。
@ModelAttribute("sportTypes")public List<SportType> populateSportTypes() {return reservationService.getAllSportTypes();}?
绑定自定义类型的属性。上述的SportType属性提交到处理程序是一个String id = "1" 的字符串类型,我们需要的是String??sportType= "棒球" 这样字段。那么这里需要如同Struts2中的转换器相同功能的实现方式:
public class SportTypeEditor extends PropertyEditorSupport { private ReservationService reservationService; public SportTypeEditor(ReservationService reservationService) { this.reservationService = reservationService; } public void setAsText(String text) throws IllegalArgumentException { int sportTypeId = Integer.parseInt(text); SportType sportType = reservationService.getSportType(sportTypeId); setValue(sportType); }}?
有了这个转换实现类,我们现在需要将其对应的控制器类联系起来,这里需要编写自己的WebBindingInitializer实现:
public class ReservationBindingInitializer implements WebBindingInitializer { private ReservationService reservationService; @Autowired public ReservationBindingInitializer(ReservationService reservationService) { this.reservationService = reservationService; } public void initBinder(WebDataBinder binder, WebRequest request) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor( dateFormat, true)); binder.registerCustomEditor(SportType.class, new SportTypeEditor( reservationService)); }}?
还需要在应用上下文中进行配置注册:
<bean /></property></bean>
?
?
表单的校验。表单成功提交前的校验在服务器端也需要做不仅是标准的做法,尤其在请求来自不支持JavaScript的wap终端时。这里可以编写如下的检验类:
@Componentpublic class ReservationValidator implements Validator { public boolean supports(Class<?> clazz) { return Reservation.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "courtName", "required.courtName", "Court name is required."); ValidationUtils.rejectIfEmpty(errors, "date", "required.date", "Date is required."); ValidationUtils.rejectIfEmpty(errors, "hour", "required.hour", "Hour is required."); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "player.name", "required.playerName", "Player name is required."); ValidationUtils.rejectIfEmpty(errors, "sportType", "required.sportType", "Sport type is required."); Reservation reservation = (Reservation) target; Date date = reservation.getDate(); int hour = reservation.getHour(); if (date != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(date); if (calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) { if (hour < 8 || hour > 22) { errors.reject("invalid.holidayHour", "Invalid holiday hour."); } } else { if (hour < 9 || hour > 21) { errors.reject("invalid.weekdayHour", "Invalid weekday hour."); } } } }}?@Component注解告诉Spring实例化该类为与类名一样的Bean,这里为reservationValidator。这里需要像激活@Controller一样的激活改类所在的包。
然后在ReservationFormController控制器中添加添加使用刚刚定义的校验器:
@Controller@RequestMapping("/reservationForm")@SessionAttributes("reservation")public class ReservationFormController { private ReservationService reservationService; private ReservationValidator validator; @Autowired public ReservationFormController(ReservationService reservationService, ReservationValidator validator) { this.reservationService = reservationService; this.validator = validator; } @ModelAttribute("sportTypes") public List<SportType> populateSportTypes() { return reservationService.getAllSportTypes(); } @RequestMapping(method = RequestMethod.GET) public String setupForm(Model model) { Reservation reservation = new Reservation(); reservation.setPlayer(new Player(username, null)); model.addAttribute("reservation", reservation); return "reservationForm"; } @RequestMapping(method = RequestMethod.POST) public String submitForm(@ModelAttribute("reservation") Reservation reservation, BindingResult result, SessionStatus status) { validator.validate(reservation, result); if (result.hasErrors()) { return "reservationForm"; } else { reservationService.make(reservation);status.setComplete(); return "redirect:/reservationSuccess"; } }}?
?这里通过ReservationValidator的校验,如果有错误则返回错提示。一旦校验成功,完成提交后,需要通过status.setComplete()来释放@SessionAttributes("reservation")中的reservation对象,以此来处理到期的控制器的回话数据。
?
7.2)向导表单。
处理跨越多页的向导表单(Wizard forms),需要为向导控制器定义多个视图,一个控制器管理所有这些表单数据的状态。现在需要使用向导表单来完成预订球场功能,请求domain代码如下:
public class PeriodicReservation { private String courtName; private Date fromDate; private Date toDate; private int period; private int hour; private Player player; //getter an setter...}?
?表单向导页依次分为如下三页:
reservationPlayerForm.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%><!-- 添加页面编码设置来解决编码问题 --><%@page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <html><head><title>Reservation Court Form</title><style>.error { color: #ff0000; font-weight: bold;}</style></head><body><form:form method="post" modelAttribute="reservation"><table> <tr> <td>Court Name</td> <td><form:input path="courtName" /></td> <td><form:errors path="courtName" css/></td> </tr> <tr> <td colspan="3"> <input type="hidden" value="0" name="_page"/> <input type="submit" value="Next" name="_target1" /> <input type="submit" value="Cancel" name="_cancel" /> </td> </tr></table></form:form></body></html>reservationTimeForm.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%><!-- 添加页面编码设置来解决编码问题 --><%@page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <html><head><title>Reservation Time Form</title><style>.error { color: #ff0000; font-weight: bold;}</style></head><body><form:form method="post" modelAttribute="reservation"><table> <tr> <td>From Date</td> <td><form:input path="fromDate" /></td> <td><form:errors path="fromDate" css/></td> </tr> <tr> <td>To Date</td> <td><form:input path="toDate" /></td> <td><form:errors path="toDate" css/></td> </tr> <tr> <td>Period</td> <td><form:select path="period" items="${periods}" /></td> <td><form:errors path="period" css/></td> </tr> <tr> <td>Hour</td> <td><form:input path="hour" /></td> <td><form:errors path="hour" css/></td> </tr> <tr> <td colspan="3"> <input type="hidden" value="1" name="_page"/> <input type="submit" value="Previous" name="_target0" /> <input type="submit" value="Next" name="_target2" /> <input type="submit" value="Cancel" name="_cancel" /> </td> </tr></table></form:form></body></html>?
reservationPlayerForm.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%><!-- 添加页面编码设置来解决编码问题 --><%@page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %> <html><head><title>Reservation Player Form</title><style>.error { color: #ff0000; font-weight: bold;}</style></head><body><form:form method="post" modelAttribute="reservation"><table> <tr> <td>Player Name</td> <td><form:input path="player.name" /></td> <td><form:errors path="player.name" css/></td> </tr> <tr> <td>Player Phone</td> <td><form:input path="player.phone" /></td> <td><form:errors path="player.phone" css/></td> </tr> <tr> <td colspan="3"> <input type="hidden" value="2" name="_page"/> <input type="submit" value="Previous" name="_target1" /> <input type="submit" value="Finish" name="_finish" /> <input type="submit" value="Cancel" name="_cancel" /> </td> </tr></table></form:form></body></html>上面三个视图中包含了pre、next、submit和cancel按钮。控制器代码如下:?
@Controller@RequestMapping("/periodicReservationForm")@SessionAttributes("reservation")public class PeriodicReservationController { private ReservationService reservationService; @SuppressWarnings("unused")private PeriodicReservationValidator validator; @Autowired public PeriodicReservationController(ReservationService reservationService, PeriodicReservationValidator periodicReservationValidator) { this.reservationService = reservationService;this.validator = periodicReservationValidator; } @RequestMapping(method = RequestMethod.GET) public String setupForm(Model model) { PeriodicReservation reservation = new PeriodicReservation(); reservation.setPlayer(new Player()); model.addAttribute("reservation", reservation); return "reservationCourtForm"; } @SuppressWarnings({ "rawtypes", "unchecked" }) @RequestMapping(method = RequestMethod.POST) public String submitForm( HttpServletRequest request, HttpServletResponse response, @ModelAttribute("reservation") PeriodicReservation reservation, BindingResult result, SessionStatus status, @RequestParam("_page") int currentPage, Model model) {Map pageForms = new HashMap();pageForms.put(0,"reservationCourtForm");// Mapped to /WEB-INF/jsp/reservationCourtForm.jsppageForms.put(1,"reservationTimeForm");// Mapped to /WEB-INF/jsp/reservationTimeForm.jsppageForms.put(2,"reservationPlayerForm");// Mapped to /WEB-INF/jsp/reservationPlayerForm.jspif (request.getParameter("_cancel") != null) { return (String)pageForms.get(currentPage); } else if (request.getParameter("_finish") != null) { new PeriodicReservationValidator().validate(reservation, result); if (!result.hasErrors()) { reservationService.makePeriodic(reservation);status.setComplete();return "redirect:reservationSuccess"; } else {return (String)pageForms.get(currentPage); }} else { int targetPage = WebUtils.getTargetPage(request, "_target", currentPage); if (targetPage < currentPage) {return (String)pageForms.get(targetPage); } switch (currentPage) { case 0: new PeriodicReservationValidator().validateCourt(reservation, result); break; case 1: new PeriodicReservationValidator().validateTime(reservation, result); break; case 2: new PeriodicReservationValidator().validatePlayer(reservation, result); break; } if (!result.hasErrors()) {return (String)pageForms.get(targetPage); } else { return (String)pageForms.get(currentPage); }} } @ModelAttribute("periods") public Map<Integer, String> periods() {Map<Integer, String> periods = new HashMap<Integer, String>();periods.put(1, "Daily");periods.put(7, "Weekly"); return periods; }}?
?这里还是用HTTP GET方法来初始化空表单,用一个HTTP POST处理方法来处理前进、后退、提交和退出等事件。不同页面中的数据都用@SessionAttributes("reservation")的唯一的会话对象中。在这里还添加对数据的校验功能,而且需要特别注意上面对每次next步骤中的数据校验,可以看到校验器代码如下:
@Componentpublic class PeriodicReservationValidator implements Validator { public boolean supports(Class<?> clazz) { return PeriodicReservation.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { validateCourt(target, errors); validateTime(target, errors); validatePlayer(target, errors); } public void validateCourt(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "courtName", "required.courtName", "Court name is required."); } public void validateTime(Object target, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "fromDate", "required.fromDate", "From date is required."); ValidationUtils.rejectIfEmpty(errors, "toDate", "required.toDate", "To date is required."); ValidationUtils.rejectIfEmpty(errors, "period", "required.period", "Period is required."); ValidationUtils.rejectIfEmpty(errors, "hour", "required.hour", "Hour is required."); } public void validatePlayer(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "player.name", "required.playerName", "Player name is required."); }}?
?这种更加细粒度的校验实现对分页表单相当来说是最好的检验方式。
?
?
参考:
juyon的blog:《spring3 MVC国际化支持之中文乱码》
Gary Mark等的书籍:《Spring Recipes》2ed