2012-02-21

為什麼透過 @InitBinder 設定的 Spring Validator 會驗證 Model 裡所有的物件?

事情的起點當然是一個 Exception:
java.lang.IllegalStateException: Invalid target for Validator [idv.neil.model.ProductValidator@5ddffa]: idv.neil.web.Pager@11ba4a2
    at org.springframework.validation.DataBinder.setValidator(DataBinder.java:498)
    at idv.neil.web.ctrl.ProductController.formBackingObject(ProductController.java:100)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.google.appengine.tools.development.agent.runtime.Runtime.invoke(Runtime.java:104)
Spring MVC 3 以後可以使用 Annotation 將 MultiActionController 與 SimpleFormController 做在同一隻 controller:

@Controller
@RequestMapping("/product")
@SessionAttributes("cmd")
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);
    private ProductService productService;

    @Autowired
    public void setProductService(ProductService productService) {
        this.productService = productService;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String form(@RequestParam(required = false) Long id, Model model) {
        Product cmd;
        if (id == null) {
            cmd = new Product();
        }
        else {
            cmd = this.productService.getProduct(id);
        }
        model.addAttribute("cmd", cmd);
        return "productForm";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String sync(@Valid @ModelAttribute("cmd") Product cmd,
            BindingResult result) {
        if (result.hasErrors()) {
            return "productForm";
        }
        if (cmd.getId() == null) {
            this.productService.addProduct(cmd);
        }
        else {
            this.productService.updateProduct(cmd);
        }
        return "redirect:/product/list.do";
    }

    @RequestMapping("/list")
    public ModelAndView list(HttpServletRequest request) {
        ModelAndView mav = new ModelAndView("productList");
        Pager pager = Pager.createPager(request,
                this.productService.countProduct());
        List<Product> list = this.productService.listProduct(
                pager.getStart(), pager.getPageSize());
        mav.addObject("list", list);
        mav.addObject(Pager.PAGER, pager);
        return mav;
    }

    @RequestMapping("/delete")
    public String delete(@RequestParam("id") Long id) {
        this.productService.deleteProduct(id);
        return "redirect:/product/list.do";
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(
                dateFormat, false));
        binder.registerCustomEditor(Text.class, new TextPropertyEditor());
        binder.setValidator(new ProductValidator());
    }
}
在 @InitBinder 裡加入 binder.setValidator(new ProductValidator()) 之前,運作一切正常,加入後,在進入列表頁時就發生上述的錯誤。

最後發現問題出現在 @InitBinder 預設會在該 @Controller 所有 request 進來時執行,此時若設有 Validator,則會針對 Model 裡的所有物件進行驗證,但有一些例外,可以在 ModelFactory.isBindingCandidate(...) 看到例外。

為避免這種情形,@InitBinder 有一個 value 參數供設定,一旦設定後,就只有 command 或 form 的 attribute name 以及 request parameter 裡有符合的名稱時,才會執行該 @InitBinder,就以上的範例來看,應該改為 @InitBinder("cmd") 就沒問題了,除非在 form 以外的 request model 裡放了該死的 cmd 屬性。

沒有留言:

張貼留言