- Spring MVC lifecycle request
- request進入1,會帶著URL, form submitted by user,.. 等資訊,此時會進入front controller servlet(DispatcherServlet)。DispatcherServlet任務是將request發送給Spring MVC controller,因此DispatcherServlet需要知道要發給誰。
- 所以DispatcherServlet會查詢一個或多個handler mapping來確定request的下一站在哪裡。handler mapping會根據request 所攜帶的URL來決定。
- 一但知道要送到哪一個controller,DispatcherServlet就會將request發送。到了controller,request會卸下其request並交給controller處理訊息。(設計良好的controller本身幾乎不處理工作,而是將業務邏輯delegate給一個或多個service objects處理)
- controller完成logic後會產生一些結果,這些結果需要返回給使用者在browser上顯示。這些資訊稱為model。這些model還會經過friendly方式格式化,通常是HTML。所以information需要給一個view呈現,通常稱為JSP。controller最後要做的一件事情就是將model data打包,並且知道哪個view需要呈現output。接著會將request連同model及view送回DispatcherServlet
- 這樣一來,controller就不會和view coupled,傳給DispatcherServlet的view name也不用特別標示為JSP。這個名字就是用來找產生真正結果的view。DispatcherServlet會使用ViewResolver來將view name mapping為特定一個的view implementation。
- 最後一站是view的implementation。在這它交付model data。request的任務也完成了。view使用model data render output。這個output會被帶回給client
from Spring in action
- 搭建Spring MVC
- Configuring DispatcherServlet
- DispatcherServlet是MVC的核心
- 使用Java將DispatcherServlet配置於Servlet container中,而不再使用web.xml
- 當deploy Servlet 3.0 container時,container會自動找到 extends AbstractAnnotationConfigDispatcherServletInitializer的class,且使用它來自動配置DispatcherServlet和Spring application context
- override 3個method
- getServletMappings: 將一個或多個path mapping到DispatcherServlet。本例為"/",也就是所有進入的request都會被這個Servlet處理
- getRootConfigClasses: return 帶有@Configuration annotation的class將會用來配置ContextLoaderListener建立的application context中的bean
- 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
- 可能引發的問題:
- 沒有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中配置中在加入內容,解決上述問題
- RootConfig: 使用@ComponentScan能夠使用不只是Web的component充實完善RootConfig
- 建立Spittr application
- 類似Twitter,再去掉字母e,所以是Spittr
- Spittr有兩個domain concepts:
- Spitter: user of the application
- 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是一個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)
- 測試方法:
- 測試中會直接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
- 藉由傳遞controller instance至MockMvcBuilders.standaloneSetup(),並使用build()建立MockMvc instance。接著再執行針對"/"的Get request設置預期的view name
- Class-level request handling
- 將path mapping設置到class-level
- 當controller在class-level上添加@RequestMapping時,這個annotation會應用到controller的所有controller method上;而controller method上的@RequestMapping會針對class-level進行補充
- 也可以增加path
- Passing model data to view
- 為了實現能夠取得Spittle list(最近更新狀態清單),需要一個能夠得到list的repositroy
- 可以藉由List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);取得最新的20個Spittle object
- Spittle class: (消息內容、timestamp、發佈時的經緯度)
- 其實Spittle就是一個很基本的POJO data object,可以進行測試
- 建立SpittleRepository interface,是由mock implement。
- 使用findSpittles() return 20個Spittle object
- 將mockRepositotry injects至SpittleController
- create mockMVC並使用這個controller
- setSingleView能夠直接設定view,可以略過view resolver
- 對/spittles發起get請求
- SpittleController
- 在addAttribute也可以直接定義key:
- model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
- spittles method也可以不使用Spring type Model,可以使用java.util.Map代替,其實功能是一樣的:
- 也可以寫成
- 這個版本沒有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>
- 則能夠顯示
- Spittles go fourth!
- 2013-09-02 (0.0 0.0)
- Spittles spittle spittle
- 2013-09-02 (0.0 0.0)
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}; } }
package spittr.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @EnableWebMvc public class 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本身 } }
@Configuration @ComponentScan(basePackages = {"spitter"}, excludeFilters = {@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)}) public class RootConfig { }
@Controller // 表示是一個Controller public class HomeController { @RequestMapping(value = "/", method = GET) //處理對/,Get的request public String home() { return "home"; // view name is home } }
<%@ 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()); } }
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 @RequestMapping("/") // 將path mapping提至class-level public class HomeController { @RequestMapping(method = GET) public String home() { return "home"; } }
@Controller @RequestMapping({"/", "/homepage"}) public class HomeController { @RequestMapping(method = GET) public String home() { return "home"; } }
package spittr.data; import java.util.List; import spittr.Spittle; public interface SpittleRepository { List<Spittle> findSpittles(long max, int count); // iD最大值,return幾個Spittle object }
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"); } }
@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;
}
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 } }
@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)); }
<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>
沒有留言:
張貼留言