之前一直在看《Spring實戰》第三版,看到第五章時發現很多東西已經過時被廢棄了,于是現在開始讀《Spring實戰》第四版了,章節安排與之前不同了,裡面應用的應該是最新的技術。
本章中,将會接觸到Spring MVC基礎,以及如何編寫控制器來處理web請求,如何通明地綁定請求參數到業務對象上,同時還可以提供資料校驗和錯誤處理的功能。
Spring MVC初探
跟蹤Spring MVC請求
請求會由DispatcherServlet配置設定給控制器(根據處理器映射來确定),在控制器完成處理後,接着請求會被發送給一個視圖來呈現結果
在請求離開浏覽器時,會帶有使用者所請求内容的資訊,例如請求的URL、使用者送出的表單資訊。
請求旅程的第一站是Spring的DispatcherServlet。Spring MVC所有的請求都會通過一個前端控制器Servlet。前端控制器是常用的Web應用程式模式,在這裡一個單執行個體的Servlet将請求委托給應用程式的其他元件來執行實際的處理。在Spring MVC中,DispatcherServlet 就是前端控制器。
DispatcherServlet的任務是将請求發送給Spring MVC控制器。控制器是一個用于處理請求的Spring元件。在典型的應用程式中可能會有多個控制器, Dispatcher Servlet需要知道應該将請求發送給哪個控制器。是以DispatcherServlet會查詢一個或多個處理器映射來确定請求的下一站在哪裡。處理器映射會根據請求所攜帶的URL資訊來進行決策。
一旦選擇了合适的控制器,DispatcherServlet會将請求發送給選中的控制器。到達了控制器,請求會卸下其負載(使用者送出的資訊)并等待控制器處理這些資訊(實際上,設計良好的控制器本身隻處理很少甚至不處理工作,而是将業務邏輯委托給個或多個服務對象)。
控制器在完成邏輯處理後通常會産生一些資訊,這些資訊需要傳回給使用者并在浏覽器上顯示。這些資訊被稱為模型(Model)。不過僅僅給使用者傳回原始的資訊是不夠的--這些資訊需要以使用者友好的方式進行格式化,一般是HTML。是以,資訊需要發送給—個視圖(View),通常會是JSP。
控制器所做的最後一件事是将模型資料打包,并且标示出用于渲染輸出的視圖名稱。它接下來會将請求連同模型和視圖名稱發送回DispatcherServlet。
這樣,控制器就不會與特定的視圖相耦合,傳遞給DispatcherServlet的視圖名稱并不直接表示某個特定的JSP。它僅僅傳遞了一個邏輯名,這個名字将會用來查找用來産生結果的真正視圖。DispatcherServlet将會使用視圖解析器來将邏輯視圖名比對為一個特定的視圖實作。
既然DispatcherServlet已經知道由哪個視圖渲染結果,那麼請求的任務基本上也就完成了。它的最後一站是視圖的實作(可能是JSP),在這裡它傳遞模型資料。請求的任務就完成了。視圖将使用模型資料渲染輸出,并通過這個輸出将響應對象傳遞給用戶端。
搭建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會預設使用
,它會通過尋找那些與視圖id比對的bean以及實作了View接口的類進行視圖解析;BeanNameViewResolver
- 沒有激活元件掃描:這樣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
進行注解的控制器。這樣就不再需要在配置類中顯式地聲明其他控制器。
接下來,添加了一個
ViewResolver
bean,即
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的數量。
為了實作分頁,需要編寫一個控制器滿足:
-
參數,結果中的spittle的id都要在這個參數之前;before
-
參數,結果中要包含的spittle的個數count
下面我們對上面的
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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。