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/yetong1985@126/blog/<c:url value="/resources/style.css" />" >
</head>
<body>
<h1>Welcome to Spitter</h1>
<a href="http://blog.163.com/yetong1985@126/blog/<c:url value="/spittles" />" >Spittles</a> |
<a href="http://blog.163.com/yetong1985@126/blog/<c:url value="/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/yetong1985@126/blog/<c:url value="/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/yetong1985@126/blog/${more_url}">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中包含预期的内容。