2011-03-02

Spring MVC 3.0.5 之一

  • Spring MVC 主要流程為 DispatcherServlet 讀取可設定的 hander mapping,將 request 傳給 handler(controller),然後再透過 view resolution 產生 response,並支援 locale 與 theme resolution 以及檔案上傳。
  • @Controller 與 @RequestMapping 組成預設的 handler。
  • Spring 3.0 以後,新增 @PathVariable等功能用來建立 RESTful 與一般 web app。
  • Spring MVC 特色:
    • 使用 POJO 作為 form backing object,不用與 Spring 有任何關聯。
    • 在 data binding 時,將型別錯誤視為驗證錯誤,而非較嚴重且難處理的系統錯誤。
    • 在 business object 的自動型別與 form field 的字串型別之間是無痛轉換的。
    • Controller 可以直接寫到 response,也可以透過 ModelAndView 來利用 view resolutaion,並使用 map 來存放 form object 或 reference data。
    • 透過 @RequestParam、@RequestHeader、 @PathVariable 等 annotation,讓 controller method 的 signature 不受任何限制。
  • Spring 的一個特色就是 Open for extension, close for modification,意思就是「擴充很容易,但不能修改」。
  • 在 web.xml 設定 DispatcherServlet:
    <servlet>
        <servlet-name>mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>mvc</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    • DispatcherServlet 預設會去找 /WEB-INF/[servlet-name]-servlet.xml,也就是 mvc-servlet.xml,來設定 WebApplicationContext。
    • WebApplicationContext 繼承 ApplicationContext,增加 web 相關的功能。
    • WebApplicationContext 放在 ServletContext 裡,可以透過 RequestContextUtils 來取得。
    • DispatcherServlet 有三個 init-param 可以設定:
      • contextClass - 實做 WebApplicationContext 的 class name,預設為 XmlWebApplicationContext。
      • contextConfigLocation - 設定檔的位置,預設為 /WEB-INF/[servlet-name]-servlet.xml,可以用逗號指定多個。
      • namespance - WebApplicationContext 的 namespace,預設為 [servlet-name]-servlet。
  • DispatcherServlet 需要某些 bean 一起工作:
    • controllers - MVC  裡的 C。
    • handler mappings -將 url 對應到特定的 controller,以及 pre-processors 或 post-processors。
    • view resolvers - 將 view name 轉換成 view 物件。
    • locale resolver - 自訂i18n。
    • theme resolver -自訂頁面風格。
    • multipart file resolver -檔案上傳。
    • handler exception resolvers - 將特定的 exception 對應到某些 view,或者自訂的 exception 處理方式。
  • DispatcherServlet 處理 request 的流程:
    • 先找出 WebApplicationContext 以 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 為名放到 request,供後續使用。
    • 找出 locale resolver 放到 request 裡,如果有的話。
    • 找出 theme resolver 放到 request 裡,如果有的話。
    • 找出 multipart file resolver 放到 request 裡,如果有的話;若有檔案上傳,則將 request 包成 MultipartHttpServletRequest,供後續使用。
    • 找出 handler,包括 pre-processors、controllers、post-processors,並執行之。
    • 得到 model 與 view, 產生 response 後回傳,handler 可能不會回傳 model 或 view,有可能有 exception 發生,或者 handler 本身已經回寫 response。
  • Controllers
    • 自從引進 @Controller 以後,Spring controllers 不用再繼承 Spring 特定的 class 了。
      @Controller
      public class HelloWorldController {
          // ...
      }
      // 當然要讓 Spring 知道怎麼找到這些 @Controller
      <context:component-scan base-package="org.springframework.samples.petclinic.web"/>
    • @RequestMapping 可以用在 class level 或 method level,用在 class level 表示以前的 SimpleFormController,用在 method level 表示 MultiActionController。
      @Controller
      @RequestMapping(value = "/user")
      public class UserController {
      
          private UserService userService;
      
          @Autowired
          public void setUserService(UserService userService) {
              this.userService = userService;
          }
      
          // 表示使用 get 讀取 /user
          @RequestMapping(method = RequestMethod.GET)
          public String userForm(Model model) {
              model.addAttribute(new User());
              return "userForm";
          }
      
      
          // 表示使用 post 讀取 /user
          @RequestMapping(method = RequestMethod.POST)
          public String adduser(User user, BindingResult result) {
              if (result.hasErrors()) {
                  return "userForm";
              }
              this.userService.addUser(user);
              return "userList/r";
          }
      
      
          // 表示使用 get 讀取 /user/list
          @RequestMapping("/list")
          public ModelAndView userList() {
              ModelAndView mav = new ModelAndView("userList");
              List<User> list = this.userService.listUser(0, 10);
              mav.addObject("list", list);
              return mav;
          }
      
      
          // 表示使用 get 讀取 /user/delete
          @RequestMapping("/delete")
          public String deleteUser(@RequestParam("id") Long id) {
              this.userService.deleteUser(id);
              return "userList/r";
          }
      }
    • 如果 @RequestMapping 同時使用在 class level 與 method level,若定義的屬性不同,則表示為聯集,若屬性相同,則表示為覆蓋。
    • 但是若同時定義路徑,即 value 屬性,則表示為路徑聯集,以上面的 userList() 為例,其所代表的路徑為 /user/list,也就是說同時定義時,method level 變成相對於 class level 的路徑。
    • 若只有定義 method level 的 @RequestMapping,即為 MultiActionController,如下所示:
      @Controller
      public class CommonController {
          @RequestMapping("/index")
          public ModelAndView index() {
              ModelAndView mav = new ModelAndView();
              return mav;
          }
      }
    • 如果因為使用 @Transactional 導致 controller 必須產生 proxy 時,若是使用 interface proxy,則必須將 @Controller 與 @RequestMapping 等 annotation 放在 interface 上,因為 proxy 之後是看不到原始 class 的,除非改用 CGLIB 的 class proxy。
    • 好用的 URI Template 與 @PathVariable
      • 將 param 參數移到 uri裡,透過 URI Template 與 @PathVariable 自動將 param 參數設定到 method 參數裡。
        // 啟用 debugging complie
        @RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
        public String findOwner(@PathVariable String ownerId, Model model) {
          //  ownerId is available here...
        }
        // 未啟用 debugging compile 時,必須在 @PathVariable 指定 param 變數名稱,但也因此 method 參數名稱就不受限制
        @RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
        public String findOwner(@PathVariable("ownerId") String owner, Model model) {
          //  ownerId is available here...
        }
        // 可以同時使用多個 @PathVariable
        @RequestMapping(value="/owners/{ownerId}/pets/{petId}", method=RequestMethod.GET)
        public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
          // ...
        } 
      • URI Template 甚至可以用在 class level 的 @RequestMapping 上。
      • 至於型別的限制上,只要有定義 data binder 就可以用。
    • 一堆 @RequestMapping 進階的功能
      • 除了支援 URI Template,也支援 Ant style path pattern,如 /user/*.do,更支援兩種混合,如 /user/*/{userId}。
      • 當沒有指定 value 屬性時,會以 method name 替代,替代方式以 MethodNameResolver 決定,預設為 InternalPathMethodNameResolver。
      • 也可以使用 params 屬性提高準確度。
        @RequestMapping(value = "/pets/{petId}", params="myParam=myValue")
      • 也可以使用 header 屬性提高準確度。
        @RequestMapping(value = "/pets", method = RequestMethod.POST, headers="content-type=text/*")
    • 可以用在 @ReqeustMapping method 裡的參數型別
      • ServletRequest, ServletReponse, HttpServletRequest, HttpServletReponse 
      • HttpSession
      • WebRequest and NativeWebRequest
      • Locale
      • InputStream and Reader - 讀取 request 的內容
      • OutputStream and Writer - 供寫出 response 的內容
      • Principal
      • @PathVariable 註解的參數
      • @RequestParam 註解的參數
      • @RequestHeader 註解的參數
      • @RequestBody 註解的參數
      • HttpEntity<?>
      • Map, Model, ModelMap - 供 view render 使用
      • Command 或 form object
      • Errors 或 BindingResult - command 或 form object 的驗證結果,在 method 參數列中一定要緊跟在驗證對象之後。
        // 無效的
        @RequestMapping(method = RequestMethod.POST)
        public String processSubmit(@ModelAttribute("pet") Pet pet, Model model, BindingResult result) { … }
        // 有效的
        @RequestMapping(method = RequestMethod.POST)
        public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result, Model model) { … }
      • SessionStatus - 2012/10/12 補充,請參考 為什麼要用 SpringMVC 的 SessionStatus
    • @ReqeustMapping method支援的回傳型別
      • ModelAndView - 夾帶 command 物件與 @ModelAttribute reference object。
      • Model - view name 由 RequestToViewNameTranslator 決定,且夾帶 command 物件與 @ModelAttribute reference object。
      • Map - view name 由 RequestToViewNameTranslator 決定,且夾帶 command 物件與 @ModelAttribute reference object。
      • View - 且夾帶 command 物件、 @ModelAttribute reference object 與 Model 物件。
      • String - 且夾帶 command 物件、 @ModelAttribute reference object 與 Model 物件。
      • void - 由 handler 自行寫入 response,或者由 RequestToViewNameTranslator 決定 view name。
      • @ResponseBody - 直接將回傳物件寫入 response,由 HttpMessageConverter 轉譯。
      • HttpEntity<?> and ResponseEntity<?> - 可以透過這兩個物件直接存取 response 的 header 和 content。
      • 其他物件 - 視為一個 model,且夾帶 command 物件與 @ModelAttribute reference object。
    • @RequestParm
      • 將 request param 塞到 method 參數。
      • 預設為 required,可以透過 required 屬性修改。
    • @RequestBody
      • 不會用。
    • @ResponseBody
      • 不會用。
    • HttpEntity<?> 類似於 @RequestBody 與 @ResponseBody。
    • @ModelAttribute
      • 用在 method level 表示 reference data。
        @ModelAttribute("types")
        public List<String> findTypes() {
            List<String> list = new ArrayList<String>();
            // ...
            return list;
        }
      • 用在 method param 表示來自於 form 的 model data。
        @RequestMapping(method = RequestMethod.POST)
        public String adduser(@ModelAttribute("user") User user, BindingResult result) {
            // ...
        }
        <form:form modelAttribute="user" method="post">
            <form:input path="email" size="30" maxlength="80"/>
            <input type="submit"/>
        </form:form>
    • @SessionAttributes
      • 用在 type level,作為以前的 sessionForm 使用。
        @Controller
        @RequestMapping("/user")
        @SessionAttributes("user")
        public class UserController {
            // ...
        }
    • @CookieValue
      • 用在 method param 來取得 cookie。
        public ModelAndView index(@CookieValue("JSESSIONID") String cookie) {
            // ...
        }
    • @RequestHeader
      •  用在 method param 來取得 request header。
    • @InitBinder
      • 用在 method level,該 method param 可以用除了 command 物件、form 物件與驗證結果物件以外 @RequestMapping 支援所有的 method param,另外可以用 WebDataBinder。
      • 不可以有回傳值。
        @InitBinder
        public void initBinder(WebDataBinder binder) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
            dateFormat.setLenient(false);
            binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
        }

沒有留言:

張貼留言