2017年12月25日 星期一

Java Spring - 搭建Spring MVC (2)

  • Accept request parameter
    • Spring MVC允許多種方式將client的data傳到controller method中
      1. Query parameter
      2. Form parameter
      3. Path variable
  • 處理Query parameter
    • 翻頁功能,讓使用者可以選擇要看哪一頁
    • 假設要查看某一頁的Spittle列表,則這個list會按照最新的Spittle在前進行排序,因此下一頁的第一筆ID早於前一頁的最後一筆ID
    • 為了顯示下一頁的Spittle,需要將一個Spittle的ID傳入
    • 也可以傳入一個parameter確定要展現的Spittle counts
    • 需要寫的controller method要處理以下參數
      • before parameter
      • count parameter
    • 將spittles() method替換為使用上述兩個parameter的新spittles() method
    • 先寫測試:
    •   @Test
        public void shouldShowPagedSpittles() throws Exception {
          List<Spittle> expectedSpittles = createSpittleList(50);
          SpittleRepository mockRepository = mock(SpittleRepository.class);
          when(mockRepository.findSpittles(238900, 50)) // expected的max及count parameter
              .thenReturn(expectedSpittles);
          
          SpittleController controller = new SpittleController(mockRepository);
          MockMvc mockMvc = standaloneSetup(controller)
              .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
              .build();
      
          mockMvc.perform(get("/spittles?max=238900&count=50")) // 傳入的參數
            .andExpect(view().name("spittles"))
            .andExpect(model().attributeExists("spittleList"))
            .andExpect(model().attribute("spittleList", 
                       hasItems(expectedSpittles.toArray())));
        }
      
      • 這個測試傳入parameters
      • 測試了這個parameter存在時的controller method
    • 新的spittles controller (同時處理有參數及沒有參數時的情境)
    •   @RequestMapping(method=RequestMethod.GET)
        public List<Spittle> spittles(
            @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
            @RequestParam(value="count", defaultValue="20") int count) {
          return spittleRepository.findSpittles(max, count);
        }
      
  • 從path parameter中取得input
    • 最簡單的方法就是在controller method中加上@RequestParam("spittle_id")
    •     @RequestMapping(value = "/show", method = RequestMethod.GET)
          public String showSpittle(@RequestParam("spittle_id") long spittleId, Model model) {
              model.addAttribute(spittleRepository.findOne(spittleId));
              return "spittle";
          }
      
    • 這個method可以處理類似/spittles/show?spittle_id=12345的request
    • 從resource-orientation 角度來看,這個方法不好。/spittles/12345/spittles/show?spittle_id=12345來得好
      • /spittles/12345: 能夠識別要查詢的resource
      • /spittles/show?spittle_id=12345: 只是一個有parameter的操作
    • 以下是一個測試:
    • @Test
          public void testSpittle() throws Exception {
              Spittle expectedSpittle = new Spittle("Hello", new Date()); //expected
              SpittleRepository mockRepository = mock(SpittleRepository.class);
              when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);
      
              SpittleController controller = new SpittleController(mockRepository);
              MockMvc mockMvc = standaloneSetup(controller).build();
      
              mockMvc.perform(get("/spittles/12345")).andExpect(view().name("spittle")) // 透過路徑取得資源
                              .andExpect(model().attributeExists("spittle"))
                              .andExpect(model().attribute("spittle", expectedSpittle));
          }
      
    • 如果要處理例如spittle_id=12345的request,可以使用placeholder。
    •     @RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
          public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
              model.addAttribute(spittleRepository.findOne(spittleId));
              return "spittle";
          }
      
    • 使用@PathVariable表示不管placeholder的值是什麼,都會傳到spittleId的parameter中
    • 若@RequestMapping內的value name與@PathVariable相同(這邊都是spittleId),則@PathVariable內名稱可以省略
    •     @RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
          public String spittle(@PathVariable long spittleId, Model model) {
              model.addAttribute(spittleRepository.findOne(spittleId));
              return "spittle";
          }
      
    • 也就是如果@PathVariable裡面沒有值,會假設與placeholder parameter name相同
    • 傳到model的key是spittle
    • 如此,就能加上jsp 取得內容及時間
    • <div class="spittleView">
        <div class="spittleMessage"><c:out value="${spittle.message}" /></div>
        <div>
          <span class="spittleTime"><c:out value="${spittle.time}" /></span>
        </div>
      </div>
      
    • 如果傳送的data是少量得,則Query parameters and path parameters是合理的。但通常都會傳送大量data,那麼使用Query parameters就不是個好選擇
  • Processing forms 
    • 新建一個SpitterController來接受request展現registerForm
    • @Controller
      @RequestMapping("/spitter")
      public class SpitterController {
          @RequestMapping(value = "/register", method = GET)
          public String showRegistrationForm() {
              return "registerForm";
          }
      }
      
    • 兩個@RequestMapping要組合起來,所以變成處理/spitter/register的request
    • 測試,確認view name是對的
    • @Test
      public void shouldShowRegistration() throws Exception {
        SpitterController controller = new SpitterController();
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(get("/spitter/register"))
               .andExpect(view().name("registerForm"));
      }
      
    • JSP:
    • <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
      <%@ page session="false"%>
      <html>
      <head>
      <title>Spittr</title>
      <link rel="stylesheet" type="text/css"
        href="<c:url value="/resources/style.css" />">
      </head>
      <body>
        <h1>Register</h1>
        <form method="POST">
          First Name: <input type="text" name="firstName" /><br /> Last Name:
          <input type="text" name="lastName" /><br /> Username: <input
            type="text" name="username" /><br /> Password: <input
            type="password" name="password" /><br /> <input type="submit"
            value="Register" />
        </form>
      </body>
      </html>
      
      • Form內沒有action,因此會當表單提交時,會直接提交到與當時的URL相同的path上
  • form-handling controller
    • 當form submit發出post request後,controller需要接受form data且將之保存為Spitter object。且為了避免重複提交,browser 應該要重新到嚮導新的頁面
    • controller
    •   @RequestMapping(method=RequestMethod.POST)
        public String saveSpittle(SpittleForm form, Model model) throws Exception {
          spittleRepository.save(new Spittle(null, form.getMessage(), new Date(), 
              form.getLongitude(), form.getLatitude()));
          return "redirect:/spittles";
        }
      
    • 測試:
    • @Test
          public void shouldProcessRegistration() throws Exception {
              SpitterRepository mockRepository = mock(SpitterRepository.class);
              Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov");
              Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); // return ID
              when(mockRepository.save(unsaved)).thenReturn(saved);
      
              SpitterController controller = new SpitterController(mockRepository);
              MockMvc mockMvc = standaloneSetup(controller).build();
      
              mockMvc.perform(post("/spitter/register").param("firstName", "Jack").param("lastName", "Bauer")
       .param("username", "jbauer").param("password", "24hours").param("email", "jbauer@ctu.gov"))
                              .andExpect(redirectedUrl("/spitter/jbauer"));
      
              verify(mockRepository, atLeastOnce()).save(unsaved); // 確認save狀況
          }
      
    • 建立提交表單後的controller
    • package spittr.web;
      
      import static org.springframework.web.bind.annotation.RequestMethod.*;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      import spittr.Spitter;
      import spittr.data.SpitterRepository;
      
      @Controller
      @RequestMapping("/spitter")
      public class SpitterController {
          private SpitterRepository spitterRepository;
      
          @Autowired
          public SpitterController(SpitterRepository spitterRepository) {
              this.spitterRepository = spitterRepository;
          }
      
          @RequestMapping(value = "/register", method = GET)
          public String showRegistrationForm() {
              return "registerForm";
          }
      
          @RequestMapping(value = "/register", method = POST)
          public String processRegistration(Spitter spitter) { // 接受一個spitter當作parameter
              spitterRepository.save(spitter); // spitterRepository是被injects的parameter
              return "redirect:/spitter/" + spitter.getUsername();
          }
      }
      
    • 當看到redirect: 前綴時,就知道要重新定向。假如spitter.getUsername()是miii,則會傳送到/spitter/miii
    • 還可以使用forward前綴,當發現是forward,就會前往指定的URL path
    • 接著就新增method到controller,針對後綴有username的做處理
    •     @RequestMapping(value = "/{username}", method = GET)
          public String showSpitterProfile(@PathVariable String username, Model model) {
              Spitter spitter = spitterRepository.findByUsername(username);
              model.addAttribute(spitter);
              return "profile";
          }
      
    • profile view:
    • <h1>Your Profile</h1>
      <c:out value="${spitter.username}" /><br/>
      <c:out value="${spitter.firstName}" />
      <c:out value="${spitter.lastName}" />
      
    • 問題:應該要為表單提交進行validation,以避免值為空或太長
  • Validating forms
    • 要處理username/password是空的情況,否則會出現安全問題,不管是誰只要提交空表單就能register
    • 方法可以是保持value在合理的長度範圍內,避免這些input field的誤用
    • 可以是把邏輯放在controller程式碼內。但,與其讓校驗邏輯弄亂controller method,不如使用Spring對Java validation API(JSP-303)。
      • 從Spring3.0開始,在Spring只要使用Java validation API,不需要額為配置,只要保證在class path下有這個Java API,比如Hibernate Validator
      • 只要將annotation放到property上,從而限制這些property的值
      • annotation都在javax.validation.constrains package中
    • Java提供validation annotation
      • @AssertFalse
      • @AssertTrue
      • @DecimalMax: 值要<=給定的BigDecimalString
      • @DecimalMin: 值要>=給定的BigDecimalString
      • @Digits: 有指定的位數
      • @Future: 需要是將來的日期
      • @Max: 值要<=給定的值
      • @Min: 值要>=給定的值
      • @NotNull
      • @Null
      • @Past: 需要是過去的日期
      • @Pattern: 直要是給訂的正規表示法
      • @Size: 值必須是String, collection或data,且給定的長度要符合給定的範圍
    • 範例:
      • package spittr;
        import javax.validation.constraints.NotNull;
        import javax.validation.constraints.Size;
        import org.apache.commons.lang3.builder.EqualsBuilder;
        import org.apache.commons.lang3.builder.HashCodeBuilder;
        Annotation
        Description
         
        public class Spitter {
          private Long id;
          @NotNull
          @Size(min=5, max=16) // 不能是空的,且5-16個字
          private String username;
          @NotNull
          @Size(min=5, max=25) // 不能是空的,且5-25個字
          private String password;
          @NotNull
          @Size(min=2, max=30) // 不能是空的,且2-30個字
          private String firstName;
          @NotNull
          @Size(min=2, max=30) // 不能是空的,且2-30個字
          private String lastName;
        ... }
        
    • 開啟validate,修改controller的method processRegistration
    •     @RequestMapping(value = "/register", method = POST)
          public String processRegistration(@Valid Spitter spitter, Errors errors) {
              if (errors.hasErrors()) { // 驗證有錯誤的時候return回registerForm
                  return "registerForm";
              }
              spitterRepository.save(spitter);
              return "redirect:/spitter/" + spitter.getUsername();
          }
      
      • 重要:Errors parameter要緊跟著帶有@Valid annotation的parameter後面

沒有留言:

張貼留言