Spring MVC架構基于模型-視圖-控制器(Model-View-Controller, MVC)模式實作,能夠幫你建構像Spring架構那樣靈活和松耦合的Web應用程式。
1、Spring MVC起步
1.1、跟蹤Spring MVC的請求
請求從離開浏覽器開始到擷取響應傳回,它會經曆好多站,在每站都會留下一些資訊同時也會帶上其他資訊。圖展示了請求使用Spring MVC所經曆的所有站點。
(1)請求離開浏覽器時①,會帶有使用者所請求内容的資訊,至少會包含請求的URL。但是還可能帶有其他的資訊,例如使用者送出的表單資訊。Spring MVC所有的請求都會通過DispatcherServlet這個前端控制器(front controller)Servlet。前端控制器是常用的Web應用程式模式,在這裡一個單執行個體的Servlet将請求委托給應用程式的其他元件來執行實際的處理。
(2)DispatcherServlet的任務是将請求發送給Spring MVC控制器(controller)。控制器是一個用于處理請求的Spring元件。在典型的應用程式中可能會有多個控制器,DispatcherServlet需要知道應該将請求發送給哪個控制器。是以DispatcherServlet以會查詢一個或多個處理器映射(handler mapping)② 來确定請求的下一站在哪裡。處理器映射會根據請求所攜帶的URL資訊來進行決策。
(3)選擇了合适的控制器,DispatcherServlet會将請求發送給選中的控制器③。到了控制器,請求會卸下其負載(使用者送出的資訊)并耐心等待控制器處理這些資訊(實際上,設計良好的控制器本身隻處理很少甚至不處理工作,而是将業務邏輯委托給一個或多個服務對象進行處理)。
(4)控制器在完成邏輯處理後,通常會産生一些資訊,這些資訊需要傳回給使用者并在浏覽器上顯示。這些資訊被稱為模型(model)。不過僅僅給使用者傳回原始的資訊是不夠的——這些資訊需要以使用者友好的方式進行格式化,一般會是HTML。是以,資訊需要發送給一個視圖(view),通常會是JSP。控制器所做的最後一件事就是将模型資料打包,并且标示出用于渲染輸出的視圖名。它接下來會将請求連同模型和視圖名發送回DispatcherServlet ④。這樣控制器就不會與特定的視圖相耦合,傳遞給DispatcherServlet的視圖名并不直接表示某個特定的JSP。實際上,它甚至并不能确定視圖就是JSP。相反,它僅僅傳遞了一個邏輯名稱,這個名字将會用來查找産生結果的真正視圖。
(5)DispatcherServlet将會使用視圖解析器(view resolver)⑤ 來将邏輯視圖名比對為一個特定的視圖實作,它可能是也可能不是JSP。
(6)既然DispatcherServlet已經知道由哪個視圖渲染結果,那請求的任務基本上也就完成了。它的最後一站是視圖的實作(可能是JSP)⑥ ,在這裡它傳遞模型資料。請求的任務就完成了。
(7)視圖将使用模型資料渲染輸出,這個輸出會通過響應對象傳遞給用戶端(不會像聽上去那樣寫死)⑦
1.2、搭建Spring MVC
建立一個Web工程,名稱為spittr。注意,需要删除掉工程/WebRoot目錄下的index.jsp檔案,并把spring 4.3.13的jar包拷貝到libs檔案夾下,jstl-1.2.jar也需要添加進去。
1.2.1 配置DispatcherServlet
從Servlet 3開始,像DispatcherServlet這樣的Servlet無需配置在web.xml檔案中,可以使用Java将DispatcherServlet配置在Servlet容器中。
package spittr.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 将一個或多個路徑映射到DispatcherServlet上
*/
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
/**
* 指定配置類
* 當DispatcherServlet啟動的時候,它會建立Spring應用上下文,并加載配置類或配置檔案中所聲明的bean
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
}
擴充AbstractAnnotationConfigDispatcherServletInitializer的任意類都會自動地配置Dispatcher-Servlet和Spring應用上下文,Spring的應用上下文會位于應用程式的Servlet上下文之中。
SpitterWebInitializer擴充三個方法:
第一個方法是getServletMappings(),它會将一個或多個路徑映射到DispatcherServlet上。在本例中,它映射的是“/”,這表示它會是應用的預設Servlet。它會處理進入應用的所有請求。
第二個方法是getServletConfigClasses()。當DispatcherServlet啟動的時候,它會建立Spring應用上下文,并加載配置檔案或配置類中所聲明的bean。getServletConfigClasses()方法中,我們要求DispatcherServlet加載應用上下文時,使用定義在WebConfig配置類(使用Java配置)中的bean。
第三個方法getRootConfigClasses()。在Spring Web應用中,通常還會有另外一個應用上下文。另外的這個應用上下文是由ContextLoaderListener建立的。我們希望DispatcherServlet加載包含Web元件的bean,如控制器、視圖解析器以及處理器映射,而ContextLoaderListener要加載應用中的其他bean。這些bean通常是驅動應用後端的中間層和資料層元件。
實際上:AbstractAnnotationConfigDispatcherServletInitializer會同時建立DispatcherServlet和ContextLoaderListener。GetServleConfigClasses()方法傳回的帶有@Configuration注解的類将會用來定義DispatcherServlet應用上下文中的bean。getRootConfigClasses()方法傳回的帶有@Configuration注解的類将會用來配置ContextLoaderListener建立的應用上下文中的bean。
在本例中,根配置定義在RootConfig中,DispatcherServlet的配置聲明在WebConfig中。(AbstractAnnotationConfigDispatcherServletInitializer它隻能部署到支援Servlet 3.0的伺服器中才能正常工作)。
1.2.2 啟動Spring MVC
建立的最簡單的Spring MVC配置就是一個帶有@EnableWebMvc注解的類。但僅僅配置了@EnableWebMvc注解,還有不少問題要解決:
沒有配置視圖解析器。如果這樣的話,Spring預設會使用BeanNameViewResolver,這個視圖解析器會查找ID與視圖名稱比對的bean,并且查找的bean要實作View接口,它以這樣的方式來解析視圖。
沒有啟用元件掃描。這樣的結果就是,Spring隻能找到顯式聲明在配置類中的控制器。
這樣配置的話,DispatcherServlet會映射為應用的預設Servlet,是以它會處理所有的請求,包括對靜态資源的請求,如圖檔和樣式表(在大多數情況下,這可能并不是你想要的效果)。
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 //啟用Spring Mvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* 配置視圖解析器,名為home的視圖将被解析為/WEB-INF/views/home.jsp
* @return
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
/**
* 配置靜态資源的處理
* 此配置要求DispatcherServlet将對靜态資源的請求轉發到Servlet容器中預設的Servlet上,
* 而非使用DispatcherServlet本身來處理此類請求
*/
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
// TODO Auto-generated method stub
//super.configureDefaultServletHandling(configurer);
configurer.enable();
}
}
WebConfig使用@Component-Scan注解,是以将會掃描spitter.web包來查找元件。控制器帶有@Controller注解,這會使其成為元件掃描時的候選bean。不用再顯示配置
添加了一個ViewResolver bean:InternalResourceViewResolver。它會查找JSP檔案,在查找的時候,它會在視圖名稱上加一個特定的字首和字尾(例如,名為home的視圖将會解析為/WEB-INF/views/home.jsp)。
WebConfig還擴充了WebMvcConfigurerAdapter并重寫了其configureDefaultServletHandling()方法。通過調用DefaultServlet-HandlerConfigurer的enable()方法,我們要求DispatcherServlet将對靜态資源的請求轉發到Servlet容器中預設的Servlet上,而不是使用DispatcherServlet本身來處理此類請求。
簡單配置RootConfig
RootConfig使用了@ComponentScan注解,有很多機會可以使用非Web的元件充實RootConfig,利于日後完善類。
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={"spittr"},
excludeFilters={@Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)})
public class RootConfig {
}
2、編寫基本的控制器
在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() {
System.out.println("處理get請求");
return "home";
}
}
HomeController帶有@Controller注解,很顯然這個注解是用來聲明控制器。
HomeController是一個構造型(stereotype)的注解,它基于@Component注解。在這裡,它的目的就是輔助實作元件掃描。因為HomeController帶有@Controller注解,是以元件掃描器會自動找到HomeController,并将其聲明為Spring應用上下文中的一個bean。其實,你也可以讓HomeController帶有@Component注解,它所實作的效果是一樣的,但是在表意性上可能會差一些,無法确定HomeController是什麼元件類型。
HomeController唯一的一個方法,也就是home()方法,帶有@RequestMapping注解。它的value屬性指定了這個方法所要處理的請求路徑,method屬性細化了它所處理的HTTP方法。它傳回了一個String類型的“home”。這個String将會被Spring MVC解讀為要渲染的視圖名稱。DispatcherServlet會要求視圖解析器将這個邏輯名稱解析為實際的視圖。
鑒于配置InternalResourceViewResolver的方式,視圖名“home”将會被解析為“/WEB-INF/views/home.jsp”路徑的jsp。
home.jsp:
<%@ 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="http://blog.163.com/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" /resources/style.css" />" >
</head>
<body>
<h1>Welcome to Spitter</h1>
<a href="http://blog.163.com/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" /spittles" />" >Spittles</a> |
<a href="http://blog.163.com/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" /spitter/register" />" >Register</a>
</body>
</html>
釋出到tomcat,輸入位址:http://localhost:8080/spittr/ ,得到:
注意:如果報http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar錯,請下載下傳jstl-1.2.jar
2..1 測試控制器
從Spring 3.2開始,可以按照控制器的方式來測試Spring MVC中的控制器了,而不僅僅是作為POJO進行測試。Spring現在包含了一種mock Spring MVC并針對控制器執行HTTP請求的機制。這樣的話,在測試控制器的時候,就沒有必要再啟動Web伺服器和Web浏覽器了。
package spittr.test;
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 org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import spittr.web.HomeController;
public class HomeControllerTest {
@Test
public void testHomePage() throws Exception {
HomeController controller = new HomeController();
//搭建MOckMvc,通過參數指定一組控制器,這樣就不需要從上下文擷取了
MockMvc mockMvc = standaloneSetup(controller).build();
/*
* MockMvcRequestBuilders.get("/user/1")構造一個請求
* mockMvc.perform執行一個RequestBuilder請求,會自動執行SpringMVC的流程并映射到相應的控制器執行處理
* andExpect:添加ResultMatcher驗證規則,驗證控制器執行完成後結果是否正确
*/
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(view().name("home"));
}
}
發起了對“/”的GET請求,并斷言結果視圖的名稱為home。它首先傳遞一個HomeController執行個體到MockMvcBuilders.standaloneSetup()并調用build()來建構MockMvc執行個體。然後它使用MockMvc執行個體來執行針對“/”的GET請求并設定期望得到的視圖名稱。
2.2 定義類級别的請求處理
拆分@RequestMapping,并将其路徑映射部分放到類級别上。
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() {
System.out.println("處理get請求");
return "home";
}
}
在這個新版本的HomeController中,路徑現在被轉移到類級别的@RequestMapping上,而HTTP方法依然映射在方法級别上。當控制器在類級别上添加@RequestMapping注解時,這個注解會應用到控制器的所有處理器方法上。處理器方法上的@RequestMapping注解會對類級别上的@RequestMapping的聲明進行補充(類上和方法上的映射組合就可以完成全部的映射)。
@RequestMapping的value屬性能夠接受一個String類型的數組。到目前為止,我們給它設定的都是一個String類型的“/”,但是,我們還可以将它映射到對“/homepage”的請求:
package spittr.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller //聲明一個控制器
@RequestMapping({"/","/homepage"})
public class HomeController {
@RequestMapping(method=RequestMethod.GET) //處理對"/"的GET請求
public String home() {
System.out.println("處理get請求");
return "home";
}
}
2.3 傳遞模型資料到視圖中
在Spittr應用中,我們需要有一個頁面展現最近送出的Spittle清單。是以,我們需要一個新的方法處理這個頁面。
首先,Spittle類盡可能簡單,包含消息内容、時間戳以及Spittle釋出時對應的經緯度資訊:
清單1 Spittle.java
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(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");
}
}
然後需要定義一個資料通路的Repository。為了實作解耦以及避免陷入資料庫通路的細節之中,我們将Repository定義為一個接口,并在後面實作它。此時,我們隻需要一個能夠擷取Spittle清單的Repository。
清單2 SpittleRepository.java:
package spittr.data;
import java.util.List;
import spittr.Spittle;
public interface SpittleRepository {
/**
*
* @param max 傳回的Spittlr對象中,Spittle ID屬性的最大值
* @param count 傳回多少個Spittle對象
* @return 擷取最新的Spittle清單
*/
List<Spittle> findSpittles(long max, int count);
}
使用H2資料庫
下載下傳位址,下載下傳Platform-Independent Zip版本,然後解壓,把bin目錄下的h2-1.4.195.jar檔案拷貝到web工程WEB-INF/lib目錄下。
在config包下建立一個資料源配置類DataConfig.java.java
package spittr.config;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
//初始化資料,chema.sql檔案中包含用于建立資料表的關系
.addScript("classpath:spittr/data/schema.sql")
.setScriptEncoding("UTF-8")
.build();
}
@Bean
public JdbcOperations jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
建立資料表及初始化資料:/spittr/src/spittr/data/schema.sql
DROP TABLE IF EXISTS Spittle;
create table Spittle (
id identity,
message varchar(140) not null,
created_at timestamp not null,
latitude double,
longitude double
);
INSERT INTO Spittle (id, message, created_at, latitude, longitude) VALUES (1111, 'Spittles go fourth', '2018-02-24', 116.579618, 39.647447);
DROP TABLE IF EXISTS Spitter;
create table Spitter (
id identity,
username varchar(20) unique not null,
password varchar(20) not null,
first_name varchar(30) not null,
last_name varchar(30) not null,
email varchar(30) not null
);
修改RootConfig,引入資料源配置:
package spittr.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@Import(DataConfig.class) //導入spring jdbc配置
@ComponentScan(basePackages={"spittr"},
excludeFilters={
@ComponentScan.Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
})
public class RootConfig {
}
裝配資料庫JdbcSpittleRepository.java:
package spittr.data;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import spittr.Spittle;
@Repository
public class JdbcSpittleRepository implements SpittleRepository {
private JdbcOperations jdbc;
@Autowired
public JdbcSpittleRepository(JdbcOperations jdbc) {
this.jdbc = jdbc;
}
@Override
public List<Spittle> findSpittles(long max, int count) {
// TODO Auto-generated method stub
return jdbc.query(
"select id, message, created_at, latitude, longitude" +
" from Spittle" +
" where id < ?" +
" order by created_at desc limit ?",
new SpittleRowMapper(), new Object[] { max, count });
}
@Override
public Spittle findOne(long id) {
// TODO Auto-generated method stub
return jdbc.queryForObject(
"select id, message, created_at, latitude, longitude" +
" from Spittle" +
" where id = ?",
new SpittleRowMapper(), id);
}
private static class SpittleRowMapper implements RowMapper<Spittle> {
public Spittle mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Spittle (
rs.getLong("id"),
rs.getString("message"),
rs.getDate("created_at"),
rs.getDouble("longitude"),
rs.getDouble("latitude"));
}
}
}
SpittleController類
package spittr.web;
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.data.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){
//将spittle添加到模型中
model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
}
在spittles()方法中給定了一個Model作為參數。這樣,spittles()方法就能将Repository中擷取到的Spittle清單填充到模型中。Model實際上就是一個Map(也就是key-value對的集合),它會傳遞給視圖,這樣資料就能渲染到用戶端了。當調用addAttribute()方法并且不指定key的時候,那麼key會根據值的對象類型推斷确定。在本例中,因為它是一個List<Spittle>,是以,鍵将會推斷為spittleList。
spittles()方法所做的最後一件事是傳回spittles作為視圖的名字,這個視圖會渲染模型。
也可以顯示聲明模型的key,指定key的值:
@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model){
//将spittle添加到模型中
model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
希望使用非Spring類型的話,那麼可以用java.util.Map來代替Model:
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model){
model.put("spittleList", spittleReopsitory.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
還有另外一種奇怪的替代方案:
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(Map model){
return spittleReopsitory.findSpittles(Long.MAX_VALUE, 20));
}
它并沒有傳回視圖名稱,也沒有顯式地設定模型,這個方法傳回的是Spittle清單。當處理器方法像這樣傳回對象或集合時,這個值會放到模型中,模型的key會根據其類型推斷得出(在本例中,也就是spittleList)。而邏輯視圖的名稱将會根據請求路徑推斷得出。因為這個方法處理針對“/spittles”的GET請求,是以視圖的名稱将會是spittles(去掉開頭的斜線)。
spittles.jsp:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<html>
<head>
<title>Spitter</title>
<link rel="stylesheet" type="text/css" href="http://blog.163.com/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" /resources/style.css" />" >
</head>
<body>
<div class="listTitle">
<h1>Recent Spittles</h1>
<ul class="spittleList">
<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>
</ul>
<c:if test="${fn:length(spittleList) gt 20}">
<hr />
<s:url value="/spittles?count=${nextCount}" var="more_url" />
<a href="http://blog.163.com/[email protected]/blog/${more_url}" target="_blank" rel="external nofollow" >Show more</a>
</c:if>
</div>
</body>
</html>
輸入http://localhost:8080/spittr/spittles,通路效果如圖:
測試SpittleController
使用Spring的MockMvc來斷言新的處理器方法中所期望的行為。
package spittr.test;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
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 java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.servlet.view.InternalResourceView;
import spittr.Spittle;
import spittr.data.SpittleRepository;
import spittr.web.SpittleController;
public class SpittleControllerTest {
@Test
public void shouldShowRecentSpittles() throws Exception {
List<Spittle> expectedSpittles = createSpittleList(20);
//建立SpittleRepository接口的mock對象
SpittleRepository mockRepository = mock(SpittleRepository.class);
//設定mock對象findSpittles方法調用時的傳回值
when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
.thenReturn(expectedSpittles);
SpittleController controller = new SpittleController(mockRepository);
//注冊一個@Controller執行個體,并設定單個視圖,即視圖解析時總是解析到這一個
MockMvc mockMvc = standaloneSetup(controller)
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
.build();
mockMvc.perform(get("/spittles")) //對"/spittles"發起get請求
.andExpect(view().name("spittles"))
.andExpect(model().attributeExists("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;
}
}
上面的測試通過建立一個SpittleRepository接口的mock實作,該實作會通過findSpittles()方法傳回一個包含20個Spittle對象的集合。然後将這個bean注入到SpittleController執行個體中,并設定MockMvc使用該執行個體。
不同于HomeControllerTest,該測試使用了setSingleView(),發起一個/spittles的GET請求,并斷言視圖是否為spittles以及model是否含有一個spittleList的屬性值,在spittleList中包含預期的内容。