2012-10-12

為什麼要用 SpringMVC 的 SessionStatus

改用 SpringMVC 的 annotaion 之後第一次遇到的問題。

簡單的說,就是 @SessionAttributes("cmd") 加上 @InitBinder("cmd"),以及 binder.setValidator(new UserValidator()) 所造成的問題。

直接來看程式,這是一段還算標準的用法,用 get 進表單,用 post 儲存修改。

@Controller
@RequestMapping("/user")
@SessionAttributes("cmd")
public class UserCtrl {

  private Logger log = LoggerFactory.getLogger(this.getClass());

  private UserService userService;

  @Inject
  public UserCtrl(UserService userService) {
    this.userService = userService;
  }

  @RequestMapping(value = "/form", method = RequestMethod.GET)
  public String form(Model model, @RequestParam(required = false) Integer uid) {
    User cmd;
    if (uid == null) {
      cmd = new User();
    }
    else {
      cmd = this.userService.get(uid);
    }
    model.addAttribute("cmd", cmd);
    return "user/form";
  }

  @RequestMapping(value = "/form", method = RequestMethod.POST)
  public String sync(@Valid @ModelAttribute("cmd") User cmd, BindingResult result, HttpSession session, SessionStatus status) {
    if (result.hasErrors()) {
      return "user/form";
    }
    if (cmd.getUid() == null) {
      this.userService.add(cmd);
    }
    else {
      this.userService.update(cmd);
    }
    status.setComplete(); // 原本沒有這一行
    return "redirect:/user/list";
  }

  @ModelAttribute("editTypes")
  public EditType[] getEditTypes() {
    return EditType.values();
  }

  @InitBinder("cmd")
  public void initBinder(WebDataBinder binder) {
    binder.setValidator(new UserValidator());
  }
}
為了讓修改的資料可以自動 bind 到 entity 上,不想手工再做一次,所以使用 @SessionAttributes("cmd"),也就是在呼叫 get 時,就將 cmd 放到 session 裡,等到 post 回來時,就直接從 session 裡取出 cmd 來做資料 bind。

再來加上欄位驗證的功能,使用 Spring 的 Validator 加上 @InitBinder("cmd"),並指明只驗證 cmd 這個物件,然後加上自訂的 UserValidator。

以下就是一個標準的 Validator 用法。
public class UserValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return User.class.equals(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ...
  }
}
到目前為止一切正常,功能使用也沒有問題,一直到加了第二個 entity。

用一樣的方式加入 Group 物件,使用時在進過一種 entity 的表單後,再進另一個 entity 的列表就會出錯。
HTTP ERROR 500

Problem accessing /user/list. Reason:

    Invalid target for Validator [com.....validator.UserValidator@161b59f3]: com.....model.Role@5dae6bdb
Caused by:

java.lang.IllegalStateException: Invalid target for Validator [com.....validator.UserValidator@161b59f3]: com.....model.Role@5dae6bdb
 at org.springframework.validation.DataBinder.setValidator(DataBinder.java:498)
 at com.....web.UserCtrl.initBinder(UserCtrl.java:133)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
問題就是因為 session 裡的 cmd 沒有清掉,導致帶著 user 這個 cmd 來到 GroupCtrl 時,GroupValidator 就會死去,因為它預期是 Group 物件,不是 User 物件。

解法是 SessionStatus,當初文件有看到這東西,但是當時沒感覺,不知道它在說什麼,現在知道了。
org.springframework.web.bind.support.SessionStatus status handle for marking
form processing as complete, which triggers the cleanup of session attributes that have been indicated by the @SessionAttributes annotation at the handler type level.
就是說,使用了 @SessionAttributes 之後,Spring 無法知道什麼時候要清掉 @SessionAttributes 存進去的資料,所以要明確告知,也就是在 post 裡傳入 SessionStatus 物件,並呼叫 status.setComplete() 就可以了。
---
---
---

沒有留言:

張貼留言