一、Spring MVC起步
1.1 跟蹤Spring MVC的請求
在請求離開瀏覽器時(shí)(步驟
1
),會(huì)帶有用戶所請求內(nèi)容的信息,至少會(huì)包含請求的URL
,但是還可能帶有其他信息。
請求的第一站是前端控制器
DispatcherServlet
。SpringMVC
所有的請求都會(huì)通過這個(gè)前端控制器,前端控制器是常用的Web
應(yīng)用程序模式,在這里是一個(gè)單實(shí)例的Servlet
將請求委托給應(yīng)用程序的其他組件來執(zhí)行實(shí)際的處理。DispatcherServlet
的任務(wù)是將請求轉(zhuǎn)發(fā)給控制器(Controller
)。為了達(dá)到此目的,DispatcherServlet
需要查詢一個(gè)或多個(gè)處理器映射器(handler mapping
)(步驟2
)來確定請求所需要的控制器是哪一個(gè)(因?yàn)榭刂破鲿?huì)有很多),處理器映射器會(huì)根據(jù)請求所攜帶的URL
信息進(jìn)行決策。一旦選擇了合適的控制器,
DispatcherServlet
會(huì)將請求發(fā)送給選中的控制器(步驟3
)。到了控制器,請求會(huì)卸下其負(fù)載(用戶 提交的信息)并耐心等待控制器處理這些信息。控制器在完成邏輯處理后,通常會(huì)產(chǎn)生一些信息,這些信息需要返回給用戶并在瀏覽器上顯示。這些信息被稱為模型(
model
)。不過,僅僅給用戶返回原始的信息是不夠的——這些信息需要以用戶友好的方式進(jìn)行格式化,一般會(huì)是HTML
。所以,信息需要發(fā)送給一個(gè)視圖(view
),通常是JSP
。控制器做的最后一件事就是將模型數(shù)據(jù)打包,并且標(biāo)識(shí)出用于渲染輸出的視圖名。然后發(fā)送回
DispatcherServlet
(步驟4
)。而返回給
DispatcherServlet
的視圖名并不直接表示某個(gè)特定的JSP
,可能并不是JSP
。相反,它僅僅傳遞的是一個(gè)邏輯名稱,這個(gè)名稱用來查找產(chǎn)生結(jié)果的真正視圖。這就需要使用視圖解析器對邏輯視圖名進(jìn)行解析(步驟5
)。解析完之后就會(huì)找到相關(guān)視圖對數(shù)據(jù)進(jìn)行渲染(步驟
6
),在這里交付模型數(shù)據(jù),之后對用戶響應(yīng)(步驟7
)。
1.2 搭建Spring MVC
1.2.1 配置DispatcherServlet
按照傳統(tǒng)方式,像DispatcherServlet
這樣的Servlet
會(huì)配置在web.xml
文件中。但是借助Servlet3.1
和Spring3.1
的功能增強(qiáng),這里使用Java
將DispatcherServlet
配置在Servlet
容器中。
package spittr.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import spittr.web.WebConfig;
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
說明:
這里我們創(chuàng)建的應(yīng)用的名稱為
Spittr
。要理解上述代碼是如何工作的,可能只需要知道擴(kuò)展AbstractAnnotationConfigDispatcherServletInitializer
的任意類都會(huì)自動(dòng)地配置DispatcherServlet
和Spring
應(yīng)用上下文,Spring
的應(yīng)用上下文位于應(yīng)用程序的Servlet
上下文之中。AbstractAnnotationConfigDispatcherServletInitializer
剖析:
在Servlet3.0
環(huán)境中,容器會(huì)在類路徑中查找實(shí)現(xiàn)了javax.servlet.ServletContainerInitializer
接口的類,如果能發(fā)現(xiàn)的話,就用它來配置Servlet
容器。Spring
提供了這個(gè)接口的實(shí)現(xiàn),名為SpringServletContainerInitializer
,這個(gè)類反過來會(huì)查找WebApplicationInitializer
的類并將配置的任務(wù)交給它們來完成。Spring3.2
引入了一個(gè)便利的WebApplicationInitializer
基礎(chǔ)實(shí)現(xiàn),即AbstractAnnotationConfigDispatcherServletInitializer
。因?yàn)槲覀兊?code>SpitterWebInitializer擴(kuò)展了AbstractAnnotationConfigDispatcherServletInitializer
(同時(shí)也就實(shí)現(xiàn)了WebApplicationInitializer
),因此部署到Servlet3.0
容器中的時(shí)候,容器會(huì)自動(dòng)發(fā)現(xiàn)它,并用它來配置Servlet
上下文。這里的第一個(gè)方法
getServletMappings()
,它會(huì)將一個(gè)或多個(gè)路徑映射到DispatcherServlet
上。本例中,它映射的是“/”
,這表示它會(huì)是應(yīng)用的默認(rèn)Servlet
。會(huì)處理進(jìn)入應(yīng)用的所有請求。其他兩個(gè)方法在后面說明。注意:這里使用的是
Java
方式,和web.xml
方式不同在于,應(yīng)用啟動(dòng)時(shí),容器會(huì)在類路徑下查找ServletContainerInitializer
的實(shí)現(xiàn)(SpringServletContainerInitializer
),此實(shí)現(xiàn)又查找WebApplicationInitializer
的實(shí)現(xiàn)(AbstractAnnotationConfigDispatcherServletInitializer
),這個(gè)類會(huì)創(chuàng)建DispatcherServlet
和ContextLoaderListener
,而這里應(yīng)用的配置類SpitterWebInitializer
繼承了AbstractAnnotationConfigDispatcherServletInitializer
。
1.2.2 兩個(gè)應(yīng)用上下文之間的故事
當(dāng)DispatcherServlet
啟動(dòng)的時(shí)候,會(huì)創(chuàng)建Spring
應(yīng)用上下文,并加載配置文件或配置類中所聲明的bean
。在上述代碼中的getServletConfigClasses()
方法中,我們要求DispatcherServlet
加載應(yīng)用上下文時(shí),使用定義在WebConfig
配置類中的bean
(相關(guān)代碼在后面給出)。
但是在Spring Web
應(yīng)用中,通常還會(huì)有另外一個(gè)應(yīng)用上下文。這個(gè)上下文是由ContextLoaderListener
創(chuàng)建的。一般情況是,我們希望DispatcherServlet
加載包含Web
組件的bean
,如控制器、視圖解析器以及處理器映射器,而ContextLoaderListener
要加載應(yīng)用中其他的bean
。這些bean
通常是驅(qū)動(dòng)應(yīng)用后端的中間層和數(shù)據(jù)層組件(如IoC
容器)。
實(shí)際上,AbstractAnnotationConfigDispatcherServletInitializer
會(huì)同時(shí)創(chuàng)建DispatcherServlet和ContextLoaderListener
。getServletConfigClasses()
方法返回的帶有@Configuration
注解的類將會(huì)用來定義DispatcherServlet
應(yīng)用上下文中的bean
。getRootConfgiClasses()
方法返回的帶有@Configuration
注解的類將會(huì)用來配置ContextLoaderListener
創(chuàng)建應(yīng)用上下文中的bean
。
當(dāng)然我們也可以使用傳統(tǒng)的web.xml
配置,但是其實(shí)沒有必要,而這種配置方式下的應(yīng)用必須部署到支持Servlet3.0
的服務(wù)器中才能正常工作。
1.2.3 啟用Spring MVC
有多種方式配置DispatcherServlet
,于是,啟用Spring MVC
組件的方式也不僅一種。可以使用<mvc:annotation-driven>
啟用注解驅(qū)動(dòng)的Spring MVC
(在后面說明),這里使用Java
進(jìn)行配置。
package spittr.web;
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc//啟用Spring MVC
@ComponentScan("spittr.web")//啟用組件掃描
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
//配置JSP視圖解析器
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
// 配置靜態(tài)資源的處理
configurer.enable();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 在后面進(jìn)行說明
super.addResourceHandlers(registry);
}
}
說明:
這里啟用了組件掃描,后面帶有
@Controller
注解的控制器會(huì)稱為掃描時(shí)的候選bean
。因此,我們不需要在配置類中顯式的聲明任何控制器。接下來,我們添加了一個(gè)
ViewResolver bean
。更具體來講InternalResourceViewResolver
。在后面會(huì)更為詳細(xì)的討論視圖解析器。在查找的時(shí)候,它會(huì)在視圖名稱上加一個(gè)特定的前綴和后綴(例如,名為home
的視圖將會(huì)解析為/WEB-INF/views/home.jsp
)。最后,
WebConfig
類還擴(kuò)展了WebMvcConfigurerAdapter
并重寫了其configureDefaultServletHandling()
方法。通過調(diào)用enable()
方法,我們要求DispatcherServlet
將對靜態(tài)資源的請求轉(zhuǎn)發(fā)到Servlet
容器中默認(rèn)的Servlet
上,而不是使用DispatcherServlet
本身來處理此類請求。下面給出
RootConfig
:
package spittr.config;
import java.util.regex.Pattern;
import org.springframework.context.annotation.*;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import spittr.config.RootConfig.WebPackage;
@Configuration
@ComponentScan(basePackages={"spittr"},
excludeFilters={
@Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
})
public class RootConfig {
}
說明:這里我們使用了自動(dòng)掃描配置,在后面可以使用非Web
組件來完善RootConfig
。這里的因?yàn)榍懊?code>@EnableWebMvc已經(jīng)被加載了,這里使用@excludeFilters
將其排除掉。
其實(shí)ContextLoaderListener
會(huì)根據(jù)根配置文件RootConfig
會(huì)加載相關(guān)bean
,而上述配置中將EnableWebMvc
類排除了,其實(shí)還應(yīng)該將spittr.web
包中的類排除(修改后的代碼后面給出),因?yàn)槟莻€(gè)包中的類是由DispatcherServlet
來加載的,這樣兩者就不會(huì)產(chǎn)生重復(fù)的bean
了,如果產(chǎn)生了重復(fù),則優(yōu)先使用DispatcherServlet
返回的bean
,而ContextLoaderListener
產(chǎn)生的bean
無法被調(diào)用,稱為內(nèi)存泄漏。
@Configuration
@Import(DataConfig.class)
@ComponentScan(basePackages={"spittr"},
excludeFilters={
@Filter(type=FilterType.CUSTOM, value=WebPackage.class)
})
public class RootConfig {
public static class WebPackage extends RegexPatternTypeFilter {
public WebPackage() {
super(Pattern.compile("spittr\\.web"));
}
}
}
1.3 Spittr簡介
這里Spittr
表示一個(gè)類似微博的應(yīng)用,其中Spitter
用于表示應(yīng)用的用戶,而Spittle
表示用戶發(fā)布的簡短狀態(tài)更新。
二、編寫基本的控制器
在Spring MVC
中,控制器只是方法上添加了@RequestMapping
注解的類,這個(gè)注解聲明了它們所要處理的請求。這里控制器盡可能簡單,假設(shè)控制器類要處理對“/”
的請求,并渲染應(yīng)用的首頁。
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)
public String home(){
return "home";
}
}
說明:
很顯然,這個(gè)類帶有
@Controller
注解,表示這個(gè)類是一個(gè)控制器。@Controller
是一個(gè)構(gòu)造型(stereotype
)的注解,基于@Component
注解。在這里,它的目的就是輔助實(shí)現(xiàn)組件掃描。因?yàn)檫@個(gè)類帶有@Controller
注解,因此組件掃描的時(shí)候會(huì)自動(dòng)找到此類,并將其聲明為一個(gè)bean
。HomeController
有一個(gè)方法home()
方法,帶有@RequestMapping
注解。它的value
屬性指定了這個(gè)方法所要處理的請求路徑,method
屬性細(xì)化了它所處理的HTTP
方法。在本例中,當(dāng)收到對“/”
的HTTP GET
請求時(shí),就會(huì)調(diào)用此方法處理。這里
home()
方法返回的是一個(gè)視圖的邏輯名,之后DispatcherServlet
會(huì)調(diào)用視圖解析器對其進(jìn)行解析,找到真正的視圖(/WEB-INF/views/home.jsp
)。
2.1 測試控制器
以前我們經(jīng)常使用單元測試對某個(gè)方法進(jìn)行測試,但是對于這種Web
應(yīng)用來說,測試總是要啟動(dòng)應(yīng)用和服務(wù)器,這是較為麻煩的。從Spring3.2
開始,可以按照控制器的方式來測試控制器,而不僅僅將控制器作為一個(gè)POJO
來測試。Spring
現(xiàn)在包含了一種mock Spring MVC
并針對控制器執(zhí)行HTTP
請求的機(jī)制。這樣就不需要啟動(dòng)應(yīng)用和服務(wù)器了。
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"));
}
}
說明:這里首先使用真實(shí)控制器對象初始化MockMvc
對象,然后使用perform()
方法發(fā)起GET
請求,然后使用view()
方法檢查返回的結(jié)果是否是“name”
(也就是檢查返回的邏輯視圖名是否是“name”
)。
2.2 定義類級別的請求處理
之前我們是將請求定義在方法級別上(在方法上使用@RequestMapping
注解),下面將請求定義在類級別上。
package spittr.web;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class HomeController {
@RequestMapping(method = RequestMethod.GET)
public String home(Model model) {
return "home";
}
}
說明:
這里在控制器中,路徑現(xiàn)在被轉(zhuǎn)移到類級別的
@RequestMapping
上,而HTTP
方法依然映射在方法級別上,此時(shí)這個(gè)注解會(huì)應(yīng)用到控制器的所有處理器方法上。當(dāng)然我們可以同時(shí)在方法上使用@RequestMapping
注解,此時(shí),方法級別上的注解就是對類上注解的一種補(bǔ)充,這里兩個(gè)注解合并之后(沒有方法級別上的注解)標(biāo)明home()
方法將會(huì)處理對“/”
路徑的GET
請求。這里注意,
@RequestMapping
的value
屬性能夠接收一個(gè)String
類型的數(shù)組。
@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
...
}
此時(shí),控制器的home()
方法能夠映射到對“/”
和“/homepage”
的GET
請求。
2.3 傳遞模型數(shù)據(jù)到視圖中
大多數(shù)控制器可能并不像上面那樣簡單,在Spittr
應(yīng)用中,我們需要一個(gè)頁面展現(xiàn)最近提交的Spittle
列表。因此,我們需要一個(gè)新的方法來處理這個(gè)頁面。
為了避免對數(shù)據(jù)庫進(jìn)行訪問,這里定義一個(gè)數(shù)據(jù)訪問的Repository
接口,稍后實(shí)現(xiàn)它。
package spittr.data;
import java.util.List;
import spittr.Spittle;
public interface SpittleRepository {
List<Spittle> findSpittles(long max, int count);
}
說明:findSpittles()
方法接受兩個(gè)參數(shù)。其中max
參數(shù)代表所返回的Spittle
中,Spittle ID
屬性的最大值,而count
參數(shù)表明要返回多少個(gè)Spittle
對象。為了獲得最新的20
個(gè)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;//經(jīng)度
private Double longitude;//緯度
public Spittle(String message, Date time) {
this(null, message, time, null, null);
}
public Spittle(Long id, String message, Date time, Double longitude, Double latitude) {
this.id = id;
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");
}
}
說明:這里唯一要注意的是使用了Apache Common Lang
包實(shí)現(xiàn)equals()
和hashCode()
方法。下面對新的控制器進(jìn)行測試。
@Test
public void houldShowRecentSpittles() 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(MockMvcRequestBuilders.get("/spittles"))
.andExpect(MockMvcResultMatchers.view().name("spittles"))
.andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))
.andExpect(MockMvcResultMatchers.model().attribute("spittleList",
Matchers.hasItems(expectedSpittles.toArray())));
}
說明:這里需要導(dǎo)入mockito-all-2.0.2-beta.jar
和hamcrest-all-1.3.jar
兩個(gè)包。這里先是使用SpittleRepository
接口創(chuàng)建了一個(gè)mock
實(shí)例,之后的when(XXX).thenReturn(XXX)
其實(shí)是一種配置,即調(diào)用某個(gè)方法應(yīng)該返回什么值。然后使用mock
對象構(gòu)造一個(gè)控制器,而這里在MockMvc
構(gòu)造器上調(diào)用setSingleView()
。這樣的話,mock
框架就不用解析控制器中的試圖名了。在很多場景中,其實(shí)沒有必要這樣做。但是對于這個(gè)控制器方法,試圖名與請求路徑非常相似,這樣如果按照默認(rèn)的視圖解析規(guī)則,MockMvc
就會(huì)發(fā)生失敗,因?yàn)闊o法區(qū)分視圖路徑和控制器路徑。之后使用perform()
方法發(fā)起GET
請求,同時(shí)對視圖和模型都設(shè)置了一些條件,即首先斷言試圖名為“spittles”
,同時(shí)斷言模型中有key
為“spittleList”
的對象同時(shí)對象中包含相關(guān)預(yù)期內(nèi)容。在運(yùn)行測試之前還需要給出相關(guān)控制器:
package spittr.web;
import java.util.Date;
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.*;
import spittr.web.Spittle;
import spittr.web.SpittleRepository;
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private static final String MAX_LONG_AS_STRING = "9223372036854775807";
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
}
說明:
這里在構(gòu)造器中注入
spittleRepository
。需要注意的是,在spittles()
方法中定義了一個(gè)Model
作為參數(shù)。這樣,此方法就能將Repository
中獲取到的Spittle
列表填充到模型中。Model
實(shí)際上就是一個(gè)Map
,它會(huì)傳遞給視圖,這樣數(shù)據(jù)就能渲染到客戶端了。當(dāng)調(diào)用addAttribute()
方法并且不指定key
的時(shí)候,那么key
會(huì)根據(jù)值的對象類型推斷。在本例中,因?yàn)樗且粋€(gè)List<Spittle>
,因此,key
推斷為spittleList
。最后返回視圖名spittles
。當(dāng)然在使用
addAttribute()
方法的時(shí)候也可以指定key
:
@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
- 如果你希望使用非
Spring
類型的話,那么可以使用Map
來代替Model
:
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model) {
model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
- 還有另一種方式來編寫
spittles()
方法:
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
return spittleRepository.findSpittles(Long.MAX_VALUE, 20);
}
當(dāng)處理器方法像這樣返回對象或集合時(shí),這個(gè)返回值會(huì)放到模型中,模型key
會(huì)根據(jù)其類型推斷得出(此處為spittleList
),而邏輯視圖名會(huì)根據(jù)請求路徑推斷出,因?yàn)檫@個(gè)方法處理針對“/spittles”
,于是邏輯視圖名為spittles
。下面給出/WEB-INF/views/spittles.jsp
:
<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>
此處使用了一些JSTL
表達(dá)式。