2017年12月25日 星期一

Java Spring - 搭建Spring MVC (1)

  • Spring MVC lifecycle request
  •  
    from Spring in action

      1. request進入1,會帶著URL, form submitted by user,.. 等資訊,此時會進入front controller servlet(DispatcherServlet)。DispatcherServlet任務是將request發送給Spring MVC controller,因此DispatcherServlet需要知道要發給誰。
      2. 所以DispatcherServlet會查詢一個或多個handler mapping來確定request的下一站在哪裡。handler mapping會根據request 所攜帶的URL來決定。
      3. 一但知道要送到哪一個controller,DispatcherServlet就會將request發送。到了controller,request會卸下其request並交給controller處理訊息。(設計良好的controller本身幾乎不處理工作,而是將業務邏輯delegate給一個或多個service objects處理)
      4. controller完成logic後會產生一些結果,這些結果需要返回給使用者在browser上顯示。這些資訊稱為model。這些model還會經過friendly方式格式化,通常是HTML。所以information需要給一個view呈現,通常稱為JSP。controller最後要做的一件事情就是將model data打包,並且知道哪個view需要呈現output。接著會將request連同model及view送回DispatcherServlet
      5. 這樣一來,controller就不會和view coupled,傳給DispatcherServlet的view name也不用特別標示為JSP。這個名字就是用來找產生真正結果的view。DispatcherServlet會使用ViewResolver來將view name mapping為特定一個的view implementation。
      6. 最後一站是view的implementation。在這它交付model data。request的任務也完成了。view使用model data render output。這個output會被帶回給client
  • 搭建Spring MVC
  • Configuring DispatcherServlet
    • DispatcherServlet是MVC的核心
    • 使用Java將DispatcherServlet配置於Servlet container中,而不再使用web.xml
    • package spittr.config;
      
      import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
      
      public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
          @Override
          protected String[] getServletMappings() { // 將DispathcerServlet mapping至("/")
              return new String[] {"/"};
          }
      
          @Override
          protected Class<?>[] getRootConfigClasses() { // ContextLoaderListener
              return new Class<?>[] {RootConfig.class};
          }
      
          @Override
          protected Class<?>[] getServletConfigClasses() {//指定config class,DispatcherServlet
              return new Class<?>[] {WebConfig.class};
          }
      
      
    • 當deploy Servlet 3.0 container時,container會自動找到 extends AbstractAnnotationConfigDispatcherServletInitializer的class,且使用它來自動配置DispatcherServlet和Spring application context
    • override 3個method
      1. getServletMappings: 將一個或多個path mapping到DispatcherServlet。本例為"/",也就是所有進入的request都會被這個Servlet處理
        1. getRootConfigClasses: return 帶有@Configuration annotation的class將會用來配置ContextLoaderListener建立的application context中的bean
        2. getServletConfigClasses: return 帶有@Configuration annotation的class將會用來定義DispatcherServlet application context中的bean
    • 使用AbstractAnnotationConfigDispatcherServletInitializer配置DispatcherServlet是傳統web.xml的替代方案。在Tomcat7之後才支援
  • Two application context
    • 當DispatcherServlet啟動的時候,會create Spring application context,並load config或config內中的bean
    • getServletConfigClasses()就是要求DispatcherServlet load application context,且使用定義在WebConfig.class的config中的bean(表示此使用的是Java configuration)
    • 但是在Spring Web application中,通常還會有另一個application context,這是由ContextLoaderListener 建立的。
    • DispatcherServlet能夠load包含web component 的bean,如controller、 view resolver等,而ContextLoaderListener能夠load application中的其他bean,這些bean通常是中間層及data-tier的component
    • 實際上,AbstractAnnotationConfigDispatcherServletInitializer會同時建立DispatcherServlet及ContextLoaderListener
  • Enable Spring MVC
    • 有很多種方式可以配置DispatcherServlet,最簡單的是帶有@EnableWebMvc annotation的class
    • package spittr.config;
      
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.EnableWebMvc;
      
      @Configuration
      @EnableWebMvc
      public class WebConfig {
      }
      
    • 可能引發的問題:
      • 沒有view resolver。Spring會默認使用BeanNameViewResolver。這個view resolver會找ID和view name相同的bean,且找的bean要implement view interface。
      • 沒有enable component scanner,則Spring只能找到在config class中的controller
      • DispatcherServlet會mapping到所有默認的Servlet,處理所有的request,包含對static resource的request,如圖片和stylesheets
    • 因此,需要再WebConfig中配置中在加入內容,解決上述問題
    • @Configuration
      @EnableWebMvc
      @ComponentScan("spitter.web")// scan 所有spitter.web package找component
      public class WebConfig extends WebMvcConfigurerAdapter {
          @Bean
          public ViewResolver viewResolver() { // config JSP view resolver
              InternalResourceViewResolver resolver = new InternalResourceViewResolver();
              resolver.setPrefix("/WEB-INF/views/");
              resolver.setSuffix(".jsp"); //加入suffix
              resolver.setExposeContextBeansAsAttributes(true);
              return resolver;
          }
      
          @Override
          public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { 
              configurer.enable(); //要求對static resource轉發給servlet container中default的servlet上,而不是使DispatcherServlet本身
      
          }
      }
      
    • RootConfig: 使用@ComponentScan能夠使用不只是Web的component充實完善RootConfig
    • @Configuration
      @ComponentScan(basePackages = {"spitter"},
                      excludeFilters = {@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
      public class RootConfig {
      }
      
  • 建立Spittr application
    • 類似Twitter,再去掉字母e,所以是Spittr
    • Spittr有兩個domain concepts:
      1. Spitter: user of the application
      2. Spittle: the brief status updates that users publish
  • Write a simple controller
    • 在Spring MVC中,controller只是方法上加了@RequestMapping annotation的class,這個annotation宣告要處理的request
    • HomeController: 簡單的Spring MVC controller class
    • @Controller // 表示是一個Controller
      public class HomeController {
      
          @RequestMapping(value = "/", method = GET) //處理對/,Get的request
          public String home() { 
              return "home"; // view name is home
          }
      }
      
    • @Controller是一個stereotype的annotation,基於@Component,目的就是輔助實現component scanner。也就是他會被component scanner掃描到,並且會被宣告為一個Spring context的bean
    • 其實,也可以宣告為@Component,與@Controller效果一樣,但在表達上會差一點,因為部會知道他是屬於什麼type
    • home() return的"home"會被Spring MVC解讀為要執行的view的名字。DispatcherServlet要求view resolver將這個名字解析為真正的view name(此範例為/WEB-INF/views/home.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>Welcome to Spittr</h1>
        <a href="<c:url value="/spittles" />">Spittles</a> |
        <a href="<c:url value="/spitter/register" />">Register</a>
      </body>
      </html>
    • 測試方法:
    • public class HomeControllerTest {
          @Test
          public void testHomePage() throws Exception {
              HomeController controller = new HomeController();
              assertEquals("home", controller.home());
          }
      }
      
      • 測試中會直接call home(),且會直接回傳"home"
      • 沒有站在MVC controller view point測試
      • 沒有assert "/" get時是否使用 home() method,所以也沒有真的判斷是不是home.jsp是view的名字
    • 從Spring3.2開始,可以按照controller的方式來測試Spring MVC中的controller,而不只是作為POJO的測試
    • 包含一種mock Spring MVC並針對controller進行HTTP request的機制
    • 如此,測試時就不用啟動web service
    • package spittr.web;
      
      import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
      import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
      import org.junit.Test;
      import org.springframework.test.web.servlet.MockMvc;
      import spittr.web.HomeController;
      
      public class HomeControllerTest {
          @Test
          public void testHomePage() throws Exception {
              HomeController controller = new HomeController();
              MockMvc mockMvc = standaloneSetup(controller).build();
              mockMvc.perform(get("/")).andExpect(view().name("home"));
      // 發起對"/"的Get request,並且設定期望的view name
          }
      }
      
    • 藉由傳遞controller instance至MockMvcBuilders.standaloneSetup(),並使用build()建立MockMvc instance。接著再執行針對"/"的Get request設置預期的view name
  • Class-level request handling
    • 將path mapping設置到class-level
    • @Controller
      @RequestMapping("/") // 將path mapping提至class-level
      public class HomeController {
          @RequestMapping(method = GET)
          public String home() {
              return "home";
          }
      }
      
    • 當controller在class-level上添加@RequestMapping時,這個annotation會應用到controller的所有controller method上;而controller method上的@RequestMapping會針對class-level進行補充
    • 也可以增加path
    • @Controller
      @RequestMapping({"/", "/homepage"})
      public class HomeController {
          @RequestMapping(method = GET)
          public String home() {
              return "home";
          }
      }
      
  • Passing model data to view
    • 為了實現能夠取得Spittle list(最近更新狀態清單),需要一個能夠得到list的repositroy
    • package spittr.data;
      
      import java.util.List;
      import spittr.Spittle;
      
      public interface SpittleRepository {
          List<Spittle> findSpittles(long max, int count); // iD最大值,return幾個Spittle object
      }
      
    • 可以藉由List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);取得最新的20個Spittle object
    • Spittle class: (消息內容、timestamp、發佈時的經緯度)
    • package spittr;
      
      import java.util.Date;
      
      public class Spittle {
          private final Long id;
          private final String message;
          private final Date time;
          private Double latitude;
          private Double longitude;
      
          public Spittle(String message, Date time) {
              this(message, time, null, null);
          }
      
          public Spittle(String message, Date time, Double longitude, Double latitude) {
              this.id = null;
              this.message = message;
              this.time = time;
              this.longitude = longitude;
              this.latitude = latitude;
          }
      
          public long getId() {
              return id;
          }
      
          public String getMessage() {
              return message;
          }
      
          public Date getTime() {
              return time;
          }
      
          public Double getLongitude() {
              return longitude;
          }
      
          public Double getLatitude() {
              return latitude;
          }
      
          @Override
          public boolean equals(Object that) {
              return EqualsBuilder.reflectionEquals(this, that, "id", "time");
          }
      
          @Override
          public int hashCode() {
              return HashCodeBuilder.reflectionHashCode(this, "id", "time");
          }
      }
      
    • 其實Spittle就是一個很基本的POJO data object,可以進行測試
    •   @Test
        public void shouldShowRecentSpittles() throws Exception {
          List<Spittle> expectedSpittles = createSpittleList(20);
          SpittleRepository mockRepository = mock(SpittleRepository.class);
          when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
              .thenReturn(expectedSpittles);
      
          SpittleController controller = new SpittleController(mockRepository);
          MockMvc mockMvc = standaloneSetup(controller)
              .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
              .build();
      
          mockMvc.perform(get("/spittles"))
             .andExpect(view().name("spittles"))
             .andExpect(model().attributeExists("spittleList")) // attribute內有spittleList
             .andExpect(model().attribute("spittleList", 
                        hasItems(expectedSpittles.toArray()))); // 包含預期內容
        }
      
      ...
        private List<Spittle> createSpittleList(int count) {
              List<Spittle> spittles = new ArrayList<Spittle>();
              for (int i = 0; i < count; i++) {
                  spittles.add(new Spittle("Spittle " + i, new Date()));
              }
              return spittles;
         }
      1. 建立SpittleRepository interface,是由mock implement。
      2. 使用findSpittles() return 20個Spittle object
      3. 將mockRepositotry injects至SpittleController
      4. create mockMVC並使用這個controller
      5. setSingleView能夠直接設定view,可以略過view resolver
      6. 對/spittles發起get請求
    • SpittleController
    • package spittr.web;
      
      import java.util.List;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      import spittr.Spittle;
      import spittr.data.SpittleRepository;
      
      @Controller
      @RequestMapping("/spittles")
      public class SpittleController {
          private SpittleRepository spittleRepository;
      
          @Autowired
          public SpittleController(SpittleRepository spittleRepository) {
              this.spittleRepository = spittleRepository;
          }
      
          @RequestMapping(method = RequestMethod.GET)
          public String spittles(Model model) { //能夠將Spittle list填入model內 (model這邊就是一個Map),能夠傳給view,如此data就能render到client
              model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); // 沒有給key,會根據type決定,此例為SpittleList
              return "spittles"; // return view name
          }
      }
      
    • 在addAttribute也可以直接定義key:
      • model.addAttribute("spittleList",  spittleRepository.findSpittles(Long.MAX_VALUE, 20));
    • spittles method也可以不使用Spring type Model,可以使用java.util.Map代替,其實功能是一樣的:
      • @RequestMapping(method=RequestMethod.GET)
        public String spittles(Map model) {
          model.put("spittleList",
                  spittleRepository.findSpittles(Long.MAX_VALUE, 20));
          return "spittles";
        }
        
    • 也可以寫成
      • @RequestMapping(method=RequestMethod.GET)
        public List<Spittle> spittles() {
          return spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        }
        
      • 這個版本沒有return view name,也沒有設定model,而是直接return Spittles list
      • 如果直接return object或collection的時候,這個value會放在model中,key可以從return type推斷出
      • 而view name會根據request path推斷。因為這個method處理針對"/spittles"的get,所以view name將會是去掉斜線:spittles
    • 上述的結果都會是一樣的。model內儲存Spittle list,key為SpittleList,且Spittle list會被放入sptittles的view內。且此JSP位置在:"/WEB-INF/views/spittles.jsp"
    • JSP: 取得list,使用<c:forEach>
    • <c:forEach items="${spittleList}" var="spittle">
        <li id="spittle_<c:out value="spittle.id"/>">
          <div class="spittleMessage">
            <c:out value="${spittle.message}" />
          </div>
          <div>
          <span class="spittleTime"><c:out value="${spittle.time}" /></span>
            <span class="spittleLocation"> 
               (<c:out value="${spittle.latitude}" />
                <c:out value="${spittle.longitude}" />)
            </span>
          </div>
        </li>
      </c:forEach>
      
    • 則能夠顯示
      • Spittles go fourth!
        • 2013-09-02 (0.0 0.0)
      • Spittles spittle spittle
        • 2013-09-02 (0.0 0.0)

沒有留言:

張貼留言