《Spring實戰》學習筆記-第五章:構建Spring web應用

以前一直在看《Spring實戰》第三版,看到第五章時發現不少東西已通過時被廢棄了,因而如今開始讀《Spring實戰》第四版了,章節安排與以前不一樣了,裏面應用的應該是最新的技術。javascript

本章中,將會接觸到Spring MVC基礎,以及如何編寫控制器來處理web請求,如何通明地綁定請求參數到業務對象上,同時還能夠提供數據校驗和錯誤處理的功能。css

Spring MVC初探

跟蹤Spring MVC請求

請求會由DispatcherServlet分配給控制器(根據處理器映射來肯定),在控制器完成處理後,接着請求會被髮送給一個視圖來呈現結果
請求會由DispatcherServlet分配給控制器(根據處理器映射來肯定),在控制器完成處理後,接着請求會被髮送給一個視圖來呈現結果

在請求離開瀏覽器時,會帶有用戶所請求內容的信息,例如請求的URL、用戶提交的表單信息。html

請求旅程的第一站是Spring的DispatcherServlet。Spring MVC全部的請求都會經過一個前端控制器Servlet。前端控制器是經常使用的Web應用程序模式,在這裏一個單實例的Servlet將請求委託給應用程序的其餘組件來執行實際的處理。在Spring MVC中,DispatcherServlet 就是前端控制器。前端

DispatcherServlet的任務是將請求發送給Spring MVC控制器。控制器是一個用於處理請求的Spring組件。在典型的應用程序中可能會有多個控制器, Dispatcher Servlet須要知道應該將請求發送給哪一個控制器。因此DispatcherServlet會查詢一個或多個處理器映射來肯定請求的下一站在哪裏。處理器映射會根據請求所攜帶的URL信息來進行決策。java

一旦選擇了合適的控制器,DispatcherServlet會將請求發送給選中的控制器。到達了控制器,請求會卸下其負載(用戶提交的信息)並等待控制器處理這些信息(實際上,設計良好的控制器自己只處理不多甚至不處理工做,而是將業務邏輯委託給個或多個服務對象)。git

控制器在完成邏輯處理後一般會產生一些信息,這些信息須要返回給用戶並在瀏覽器上顯示。這些信息被稱爲模型(Model)。不過僅僅給用戶返回原始的信息是不夠的--這些信息須要以用戶友好的方式進行格式化,通常是HTML。因此,信息須要發送給—個視圖(View),一般會是JSP。github

控制器所作的最後一件事是將模型數據打包,而且標示出用於渲染輸出的視圖名稱。它接下來會將請求連同模型和視圖名稱發送回DispatcherServlet。web

這樣,控制器就不會與特定的視圖相耦合,傳遞給DispatcherServlet的視圖名稱並不直接表示某個特定的JSP。它僅僅傳遞了一個邏輯名,這個名字將會用來查找用來產生結果的真正視圖。DispatcherServlet將會使用視圖解析器來將邏輯視圖名匹配爲一個特定的視圖實現。正則表達式

既然DispatcherServlet已經知道由哪一個視圖渲染結果,那麼請求的任務基本上也就完成了。它的最後一站是視圖的實現(多是JSP),在這裏它交付模型數據。請求的任務就完成了。視圖將使用模型數據渲染輸出,並經過這個輸出將響應對象傳遞給客戶端。spring

搭建Spring MVC

配置DispatcherServlet

DispatcherServlet是Spring MVC的核心,它負責將請求分發到其餘各個組件。

在舊版本中,DispatcherServlet之類的servlet通常在web.xml文件中配置,該文件通常會打包進最後的war包種;可是Spring3引入了註解,咱們在這一章將展現如何基於註解配置Spring MVC。

注意:
在使用maven構建web工程時,因爲缺乏web.xml文件,可能會出現web.xml is missing and <failOnMissingWebXml> is set to true這樣的錯誤,那麼能夠經過在pom.xml文件中添加以下配置來避免這種錯誤:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> 

既然不適用web.xml文件,你須要在servlet容器中使用Java配置DispatcherServlet,具體的代碼列舉以下:

package spittr.config; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected String[] getServletMappings() { return new String[] { "/" }; } @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { RootConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; } } 

任意繼承自AbstractAnnotationConfigDispatcherServletInitializer的類都會被自動用來配置DispatcherServlet,這個類負責配置DispatcherServlet初始化Spring MVC容器和Spring容器

SpittrWebAppInitializer重寫了三個方法,getRootConfigClasses()方法用於獲取Spring應用容器的配置文件,這裏咱們給定預先定義的RootConfig.class;getServletConfigClasses()負責獲取SpringMVC應用容器,這裏傳入預先定義好的WebConfig.class;getServletMappings()方法負責指定須要由DispatcherServlet映射的路徑,這裏給定的是"/",意思是由DispatcherServlet處理全部向該應用發起的請求。

兩種應用上下文

當DispatcherServlet啓動時,會建立一個Spring應用上下文而且會加載配置文件中聲明的bean,經過getServletConfigClasses()方法,DispatcherServlet會加載WebConfig配置類中所配置的bean。

在Spring web應用中,一般還有另一種應用上下文:ContextLoaderListener

DispatcherServlet用來加載web組件bean,如控制器(controllers)、視圖解析器(view resolvers)以及處理器映射(handler mappings)等。而ContextLoaderListener則用來加載應用中的其餘bean,如運行在應用後臺的中間層和數據層組件。

AbstractAnnotationConfigDispatcherServletInitializer會同時建立DispatcherServlet和ContextLoaderListener。getServletConfigClasses()方法返回的@Configuration類會定義DispatcherServlet應用上下文的bean。同時,getRootConfigClasses()返回的@Configuration類用來配置ContextLoaderListener上下文建立的bean。

相對於傳統的web.xml文件配置的方式,經過AbstractAnnotationConfigDispatcherServletInitializer來配置DispatcherServlet是一種替代方案。須要注意的是,這種配置只適用於Servlet 3.0,例如Apache Tomcat 7或者更高。

激活Spring MVC

正若有多種方式能夠配置DispatcherServlet,激活Spring MVC組件也有不止一種方法。通常的,都會經過XML配置文件的方式來配置Spring,例如能夠經過<mvc:annotation-driven>來激活基於註解的Spring MVC。

最簡單的配置Spring MVC的一種方式是經過@EnableWebMvc註解:

package spittr.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @EnableWebMvc public class WebConfig { } 

@Configuration表示這是Java配置類;@EnableWebMvc註解用於啓動Spring MVC特性。

這樣就能夠激活Spring MVC了,可是還有其餘一些問題:

  • 沒有配置視圖解析器(view resolvers),這種狀況下,Spring會默認使用BeanNameViewResolver,它會經過尋找那些與視圖id匹配的bean以及實現了View接口的類進行視圖解析;
  • 沒有激活組件掃描:這樣Spring會尋找配置中明確聲明的任意控制器;
  • DispatcherServlet會處理全部的請求,包括靜態資源請求,如圖片和樣式(這些每每不是咱們想要的)。

所以,須要爲WebConfig增長一些配置:

package spittr.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.InternalResourceViewResolver; @Configuration @EnableWebMvc @ComponentScan("spitter.web") // 激活Spring MVC public class WebConfig extends WebMvcConfigurerAdapter { // 配置一個JSP視圖解析器 @Bean public ViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB_INF/views/"); resolver.setSuffix(".jsp"); resolver.setExposeContextBeansAsAttributes(true); return resolver; } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } } 

首先須要注意的是,WebConfig使用了@ComponentScan註解,所以會在spitter.web包下掃描尋找組件,這些組件包括使用@Controller進行註解的控制器。這樣就再也不須要在配置類中顯式地聲明其餘控制器。

接下來,添加了一個ViewResolverbean,即InternalResourceViewResolver。它經過匹配符合設置的前綴和後綴的視圖來用來尋找對應的JSP文件,好比視圖home會被解析爲/WEB-INF/views/home.jsp。這裏的三個函數的含義依次是:setPrefix()方法用於設置視圖路徑的前綴;setSuffix()用於設置視圖路徑的後綴,即若是給定一個邏輯視圖名稱——"home",則會被解析成"/WEB-INF/views/home.jsp"; setExposeContextBeansAsAttributes(true)使得能夠在JSP頁面中經過${}訪問容器中的bean。

而後,WebConfig繼承自WebMvcConfigurerAdapter,而且重寫了configureDefaultServletHandling()方法,經過調用enable()方法從而可讓DispatcherServlet將靜態資源的請求轉發給默認的servlet。

package spittr.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @ComponentScan(basePackages = { "spitter" }, excludeFilters = { @Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class) }) public class RootConfig { } 

須要注意的一點是,RootConfig 使用了@ComponentScan註解。

Spittr應用介紹

這一章要用的例子應用,從Twitter獲取了一些靈感,所以最開始叫Spitter;而後又借鑑了最近比較流行的網站Flickr,所以咱們也把e去掉,最終造成Spittr這個名字。這也有利於區分領域名稱(相似於twitter,這裏用spring實現,所以叫spitter)和應用名稱。

Spittr相似於Twitter,用戶能夠經過它添加一些推文。Spittr有兩個重要的概念:spitter(應用的用戶)和spittle(用戶發佈簡單狀態)。本章將會構建該應用的web層、建立用於展現spittle的控制器以及用戶註冊流程。

編寫簡單的控制器

Spring MVC中,控制器僅僅是擁有@RequestMapping註解方法的類,從而能夠聲明它們能夠處理何種請求。

在開始以前,咱們先假設一個控制器,它能夠處理匹配/的請求並會跳轉到主頁面。

package spittr.web; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller // 聲明一個控制器 public class HomeController { @RequestMapping(value = "/", method = RequestMethod.GET) // 處理GET請求 public String home() { return "home"; } } 

@Controller是一個構造型註解,它基於@Component,組件掃描器會自動地將HomeController聲明爲Spring上下文的一個bean。

home()方法採用了@RequestMapping註解,屬性value指定了該方法處理的請求路徑,method方法指定了能夠處理的HTTP方法。這種狀況下,當一個來自/的GET方法請求時,就會調用home()方法。

home()方法僅僅返回了一個"home"的String值,Spring MVC會對這個String值進行解析並跳轉到指定的視圖上。DispatcherServlet則會請求視圖解析器將這個邏輯視圖解析到真實視圖上。

咱們已經配置了InternalResourceViewResolver,「home」視圖會被解析到/WEB-INF/views/home.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <meta charset="utf-8"> <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> 

下面對HomeController進行測試。

測試控制器

通常的web測試須要將工程發佈到一個web容器中,啓動後才能觀察運行結果。
如:


主頁
主頁

從另外的角度來看,HomeController實際上是一個簡單的POJO對象,那麼可使用下面的方法對其進行測試:

package spittr.web; import org.junit.Test; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; public class HomeControllerTest { @Test public void testHomePage() throws Exception { HomeController controller = new HomeController(); // 設置MockMvc MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.view().name("home")); } } 

相對於直接調用home()方法測試它的返回值,上面的測試中發起了一個來自/的 GET 請求,而且對其結果視圖進行斷言。將HomeController的實例傳送給MockMvcBuilders.standaloneSetup,而且調用build()方法來建立一個MockMvc實例。而後,使用MockMvc實例產生了一個GET請求,而且設置了視圖的指望。

定義類層級的請求處理

package spittr.web; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller // 聲明一個控制器 @RequestMapping("/") // 控制器匹配路徑 public class HomeController { @RequestMapping(method = RequestMethod.GET) // 處理GET請求 public String home() { return "home";// 視圖名稱 } } 

在這個新版的HomeController中,將請求匹配路徑移到了類層級,HTTP方法的匹配仍處在方法層級。當有控制類中有一個類層級的@RequestMapping,該類中全部的用@RequestMapping註解的處理方法共同組成了類層級的@RequestMapping

@RequestMapping的value屬性接受String數組,那麼就可使用以下配置:

@Controller // 聲明一個控制器 @RequestMapping("/", "/homepage") // 控制器匹配路徑 public class HomeController { ... } 

這種狀況下,home()方法就能夠處理來自//homepage的GET請求。

將model數據傳送給視圖

在Spittr應用中,須要一個頁面,用來顯示最近提交的spittle清單。首先須要定義一個數據訪問的倉庫,用來抓取spittle:

package spittr.data; import java.util.List; import spittr.Spittle; public interface SpittleRepository { /** * @param max * 待返回的最大的Spittle ID * @param count * 返回Spittle對象的個數 * @return */ List<Spittle> findSpittles(long max, int count); } 

若是要獲取最近的20個Spittle對象,那麼只需調用這樣調用:
List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);

下面對Spittle進行定義:

package spittr; import java.util.Date; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; 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 latitude, Double longitude) { this.id = null; this.message = message; this.time = time; this.latitude = latitude; this.longitude = longitude; } @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that, "id", "time"); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, "id", "time"); } //getters and setters } 

Spittle對象中如今包含信息、時間戳、位置這幾個屬性。

下面利用Spring的MockMvc來斷言新的控制器的行爲是否正確:

 
  

上面的測試經過建立一個SpittleRepository接口的mock實現,該實現會經過findSpittles()方法返回一個包含20個Spittle對象的集合。而後將這個bean注入到SpittleController實例中,並設置MockMvc使用該實例。

不一樣於HomeControllerTest,該測試使用了setSingleView(),發起一個/spittles的GET請求,並斷言視圖是否爲spittles以及model是否含有一個spittleList的屬性值。

固然,如今運行這個測試代碼確定是會出錯的,由於尚未SpittleController。

package spittr.web; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.hamcrest.core.IsCollectionContaining; import org.junit.Test; import org.mockito.Mockito; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.InternalResourceView; import spittr.Spittle; import spittr.data.SpittleRepository; public class SpittleControllerTest { @Test public void shouldShowRecentSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(20); SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class); Mockito.when(mockRepository.findSpittles(Long.MAX_VALUE, 20)).thenReturn(expectedSpittles); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB_INF/views/spittles.jsp")).build(); // 調用MockMvc.perform(RequestBuilder requestBuilder)發起一個http請求,而後將獲得ResultActions mockMvc.perform(MockMvcRequestBuilders.get("/spittles"))// 添加驗證斷言來判斷執行請求後的結果是不是預期的; .andExpect(MockMvcResultMatchers.view().name("spittles"))// view():獲得視圖驗證器; // 獲得相應的***ResultMatchers後,接着再調用其相應的API獲得ResultMatcher, // 如ModelResultMatchers.attributeExists(final String... names)判斷Model屬性是否存在。 .andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))// model():獲得模型驗證器; .andExpect(MockMvcResultMatchers.model().attribute("spittleList", IsCollectionContaining.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 ", new Date())); } return spittles; } } 

SpittleController中,使用@Autowired註解注入了spittleRepository屬性。

須要注意的是spittles()方法使用了Model(控制器和視圖之間傳遞的數據)做爲入參,Model本質上是一個map,它會被傳送至view,所以數據能夠提供給客戶端。若是在調用addAttribute()方法時沒有指定key,那麼就會從傳入的對象中獲取,好比代碼中傳入的參數屬性是List<Spittle>,那麼key就是spittleList。最後,該方法返回spittles做爲傳動給model的視圖名稱。

也能夠顯示的指定key:

model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); 

也能夠直接採用map的方式:

@RequestMapping(method = RequestMethod.GET) public String spittles(Map model) { // 將spittles添加到model中 model.put("spittles", spittleRepository.findSpittles(Long.MAX_VALUE, 20)); // 返回視圖名稱 return "spittles"; } 

無論採用何種方式實現spittles()方法,結果都是同樣的。一個Spittle對象集合會存儲在model中,並分配到名爲spittles的view中,根據測試方法中的配置,該視圖就是/WEB-INF/views/spittles.jsp。

如今model已經有數據了,那麼JSP頁面中如何獲取數據呢?當視圖是一個JSP頁面時,model數據會做爲請求屬性被拷貝到請求中,所以能夠經過JSTL(JavaServer Pages Standard Tag Library)<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> 

下面對SpittleController進行擴展,讓它能夠處理一些輸入。

接受輸入請求

Spring MVC提供了以下方式供客戶端傳遞數據到控制器處理方法:

  • Query parameters
  • Form parameters
  • Path variables

處理查詢參數:@RequestParam

Spittr應用的一個需求就是要對spittle列表分頁展現,可是SpittleController僅僅展現最近的spittle。若是要讓用戶能夠每次獲得一頁的spittle記錄,那麼就須要讓用戶能夠經過某種方式將他們想看的spittle記錄的參數傳遞到後臺。

在瀏覽spittle時,若是想要查看下一頁的spittle,那麼就須要傳遞比當前頁的最後一個spittle的id小一位的id,也能夠傳遞想要展現的spittle的數量。

爲了實現分頁,須要編寫一個控制器知足:

  • before參數,結果中的spittle的id都要在這個參數以前;
  • count參數,結果中要包含的spittle的個數

下面咱們對上面的spittles()方法進行小小的改動,讓它可使用before和count參數。首先對測試方法進行改動:

@Test public void shouldShowRecentSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(20); SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class); Mockito.when(mockRepository.findSpittles(238900, 50)).thenReturn(expectedSpittles); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB_INF/views/spittles.jsp")).build(); // 調用MockMvc.perform(RequestBuilder requestBuilder)發起一個http請求,而後將獲得ResultActions mockMvc.perform(MockMvcRequestBuilders.get("/spittles?max=238900&count=50"))// 添加驗證斷言來判斷執行請求後的結果是不是預期的; .andExpect(MockMvcResultMatchers.view().name("spittles"))// view():獲得視圖驗證器; // 獲得相應的***ResultMatchers後,接着再調用其相應的API獲得ResultMatcher, // 如ModelResultMatchers.attributeExists(final String... names)判斷Model屬性是否存在。 .andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))// model():獲得模型驗證器; .andExpect(MockMvcResultMatchers.model().attribute("spittleList", IsCollectionContaining.hasItems(expectedSpittles.toArray()))); } 

這個測試方法的主要改動就是它發起的GET請求傳遞了兩個參數:max和count。對spittles()進行修改:

@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); } 

這種狀況下,若是沒有max參數沒有指定,那麼就會使用默認的設置。因爲查詢參數是String類型的,所以defaultValue屬性值也須要設置爲String類型,須要對Long.MAX_VALUE進行設置:
private static final String MAX_LONG_AS_STRING = "9223372036854775807";

雖然,這裏defaultValue的屬性爲String類型,當運行到函數時,將會根據函數的參數類型進行轉換。

查詢參數是請求中傳送信息給控制器的最經常使用方式,另一種流行的方式就是將參數做爲請求路徑的一部分。

經過路徑參數傳遞數據:@PathVariable

假設如今應用須要展現單獨的一篇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"; } 

這個handler方法將會處理形如/spittles/show?spittle_id=12345的請求,可是這並不符合資源導向的觀點。理想狀況下,應該使用URL路徑對資源進行區分,而不是查詢參數,即應該使用/spittles/12345這種形式。

爲了實現資源導向的控制器,咱們先在測試中得到這個需求(使用了靜態引入):

@Test public void testSpittle() throws Exception { Spittle expectedSpittle = new Spittle("Hello", new Date()); SpittleRepository mockRepository = Mockito.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)); } 

該測試中發起了一個/spittles/12345的GET請求,而且對其返回結果視圖進行斷言。

爲了知足路徑參數,Spring MVC容許在@RequestMapping路徑中使用佔位符(須要用大括號包圍),下面是使用佔位符來接受一個id做爲路徑的一部分:

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET) public String spittle( @PathVariable("spittleId") long spittleId, Model model ) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; } 

spittle()方法的spittleId入參使用了@PathVariable("spittleId")註解,代表請求中佔位符位置的值都會被傳送到handler的spittleId參數。@RequestMapping中value屬性的佔位符必須和@PathVariable包裹的參數一致。若是@PathVariable中沒有給定參數,那麼將默認使用入參的冊數參數名。便可以使用下面的方法:

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET) public String spittle( @PathVariable long spittleId, Model model ) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; } 

spittle()方法會將接收的參數值傳遞給spittleRepository的findOne()方法並查找到一個Spittle,並將其放置到model中,model的key值會是spittle,接下來就能夠在視圖中引用這個Spittle:

<div class="spittleView"> <div class="spittleMessage"> <c:out value="${spittle.message}" /> </div> <div> <span class="spittleTime"><c:out value="${spittle.time}" /></span> </div> </div> 

查詢參數和路徑參數能夠處理一些少許的請求數據,可是當請求數據過大時,它們就再也不適用,下面就來說解一下如何處理表單數據。

處理表單

Web應用不只僅是將內容推送給用戶,它同時也會讓用戶填寫表單並將數據提交給應用。

對於表單有兩種處理方式:展現表單以及處理用戶提交的表單數據。在Spittr中,須要提供一個供新用戶進行註冊的表單。

SpitterController:展現用戶註冊表單

package spittr.web; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/spitter") public class SpitterController { // 處理來自/spitter/register的get請求 @RequestMapping(value = "/register", method = RequestMethod.GET) public String showRegistrationForm() { return "registerForm"; } } 

showRegistrationForm方法的@RequestMapping註解,以及類級別的註解@RequestMapping,代表了這個方法會處理來自/spitter/register的get請求,該方法僅僅返回了一個名爲registerForm的邏輯視圖。根據以前在InternalResourceViewResolver中的配置,這個邏輯視圖會導向到/WEB-INF/views/registerForm.jsp該界面。

對應的測試方法:

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; public class SpitterControllerTest { @Test public void shouldShowRegistration() throws Exception { SpitterController controller = new SpitterController(); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(get("/spitter/register")).andExpect(view().name("registerForm")); } } 

也能夠經過啓動項目訪問界面的方式驗證:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ page session="false" %> <html> <head> <title>Spitter</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> 
該界面提供了用戶註冊的功能
該界面提供了用戶註冊的功能

接下來須要對提交的表單進行處理。

編寫表單處理控制器

在處理POST請求時,控制器須要接受表單數據而且將這些數據存儲爲一個Spitter對象。爲了不重複提交,應該重定向到一個新的界面:用戶信息頁。在處理post請求時,一個聰明的作法就是在處理完成後發送一個重定向的請求,從而能夠避免重複提交。

下面來實現控制器方法,從而能夠處理註冊請求。

private SpitterRepository spitterRepository; public SpitterController() { } // 注入SpitterRepository @Autowired public SpitterController(SpitterRepository spitterRepository) { this.spitterRepository = spitterRepository; } public String processRegistration(Spitter spitter) { // 保存Spitter spitterRepository.save(spitter); // 重定向到新的頁面 return "redirect:/spitter/" + spitter.getUsername(); } 

processRegistration方法使用Spitter對象做爲入參,該對象的屬性會從請求中填充。該方法中調用了spitterRepository的save方法對Spitter對象進行存儲。最後返回了一個帶有redirect:的字符串。

當InternalResourceViewResolver遇到redirect:時,它會自動地將其當作一個重定向請求,從而能夠重定向到用戶詳情頁面,如/spitter/xiaoming。

同時,InternalResourceViewResolver也能夠識別前綴forward:,這種狀況下,請求會被轉向到給定的URL地址。

下面須要編寫處理處理用戶詳情頁面的方法:

@RequestMapping(value = "/{username}", method = RequestMethod.GET) public String showSpitterProfile(@PathVariable("username") String username, Model model) { Spitter spitter = spitterRepository.findByUsername(username); model.addAttribute(spitter); return "profile"; } 

參數校驗

從Spring3.0開始,Spring支持Java校驗api,從而能夠從而能夠不須要添加其餘配置,僅僅須要有一個Java API 的實現,如Hibernate Validator。

Java Validation API定義了許多註解,可使用這些註解來約束參數的值,全部的註解都在包javax.validation.constraints中。

註解 描述
@AssertFalse(@AssertTrue) 對象必須是布爾類型,而且必須爲false(true)
@DecimalMax(value)、@DecimalMin(value) 限制對象必須是一個數字,其值不大於(不小於)指定的BigDecimalString值
@Digits(integer,fraction) 對象必須爲一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Future 必須是一個未來的日期
@Max(value)、@Min(value) 必須爲一個不大於(不小於)指定值的數字
@NotNull 限制對象不能爲空
@Null 限制對象必須爲空
@Past 必須是一個過去的日期
@Pattern(value) 必須符合指定的正則表達式
@Size(min,max) 限制字符長度必須在min到max之間

使用示例:

public class Spitter { private Long id; @NotNull @Size(min = 5, max = 16) private String username; @NotNull @Size(min = 5, max = 25) private String password; @NotNull @Size(min = 2, max = 30) private String firstName; ... 

既然已經對Spitter的參數添加了約束,那麼就須要改動processRegistration方法來應用校驗:

@RequestMapping(value = "/register", method = RequestMethod.POST) public String processRegistration(@Valid Spitter spitter, Errors errors) { // 若校驗中出現錯誤,那麼就返回到註冊界面 if (errors.hasErrors()) { return "registerForm"; } // 保存Spitter spitterRepository.save(spitter); // 重定向到新的頁面 return "redirect:/spitter/" + spitter.getUsername(); } 

總結

這一章比較適合Spring MVC的入門學習資料。涵蓋了Spring MVC處理web請求的處理過程、如何寫簡單的控制器和控制器方法來處理Http請求、如何使用mockito框架測試控制器方法。

基於Spring MVC的應用有三種方式讀取數據:查詢參數、路徑參數和表單輸入。本章用兩節介紹了這些內容,並給出了相似錯誤處理和參數驗證等關鍵知識點。

因爲缺乏真正的入庫操做,所以本章節的一些方法不能真正的運做。

在接下來的章節中,咱們會對Spring視圖進行深刻了解,對如何在JSP頁面中使用Spring標籤庫進行展開。

做者:hoxis 連接:https://www.jianshu.com/p/74357110e4cc 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索