到现在为止,就编写超级简单的控制器来说,HomeController 已经是一个不错的样例了。但是大多数的控制器并不是这么简单。在 Spittr 应用中,我们需要有一个页面展现最近提交的 Spittle 列表。因此,我们需要一个新的方法来处理这个页面。
首先,需要定义一个数据访问的 Repository。为了实现解耦以及避免陷入数据库访问的细节之中,我们将 Repository 定义为一个接口,并在稍后实现它(第 10 章中)。此时,我们只需要一个能够获取 Spittle 列表的 Repository,如下所示的 SpittleRepository 功能已经足够了:
package spittr.data;
import java.util.List;
import spittr.Spittle;
public interface SpittleRepository {
List<Spittle> findSpittles(long max, int count);
}
现在,我们让 Spittle 类尽可能的简单,如下面的程序清单 5.8 所示。它的属性包括消息内容、时间戳以及 Spittle 发布时对应的经纬度。
程序清单 5.8 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(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");
}
}
就大部分内容来看,Spittle 就是一个基本的 POJO 数据对象 —— 没有什么复杂的。唯一要注意的是,我们使用 Apache Common Lang 包来实现 equals() 和 hashCode() 方法。这些方法除了常规的作用以外,当我们为控制器的处理器方法编写测试时,它们也是有用的。
既然我们说到了测试,那么我们继续讨论这个话题并为新的控制器方法编写测试。如下的程序清单使用 Spring 的 MockMvc 来断言新的处理器方法中你所期望的行为。
程序清单 5.9 测试 SpittleController 处理针对 “/spittles” 的 GET 请求
@Test
public void shouldShowPagedSpittles() throws Exception {
List<Spittle> expectedSpittles = createSpittleList(50);
SpittleRepository mockRepository = mock(SpittleRepository.class);
when(mockRepository.findSpittles(238900, 50))
.thenReturn(expectedSpittles);
SpittleController controller = new SpittleController(mockRepository);
MockMvc mockMvc = standaloneSetup(controller)
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
.build();
mockMvc.perform(get("/spittles?max=238900&count=50"))
.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 对象。然后,它将这个 Repository 注入到一个新的 SpittleController 实例中,然后创建 MockMvc 并使用这个控制器。
需要注意的是,与 HomeController 不同,这个测试在 MockMvc 构造器上调用了 setSingleView()。这样的话,mock 框架就不用解析控制器中的视图名了。在很多场景中,其实没有必要这样做。但是对于这个控制器方法,视图名与请求路径是非常相似的,这样按照默认的视图解析规则时,MockMvc 就会发生失败,因为无法区分视图路径和控制器的路径。在这个测试中,构建 Internal-ResourceView 时所设置的实际路径是无关紧要的,但我们将其设置为与InternalResourceViewResolver 配置一致。
这个测试对 /spittles
发起 GET 请求,然后断言视图的名称为 spittles 并且模型中包含名为 spittleList 的属性,在 spittleList 中包含预期的内容。 当然,如果此时运行测试的话,它将会失败。它不是运行失败,而是在编译的时候就会失败。这是因为我们还没有编写 SpittleController。现在,我们创建Spittle-Controller,让它满足程序清单 5.9 的预期。如下的 SpittleController 实现将会满足以上测试的要求。
程序清单 5.10 SpittleController:在模型中放入最新的 spittle 列表
package spittr.web;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import spittr.Spittle;
import spittr.data.SpittleRepository;
@Controller
@RequestMapping("/spittles")
public class SpittleController {
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"
}
}
我们可以看到 SpittleController 有一个构造器,这个构造器使用了 @Autowired 注解,用来注入 SpittleRepository。这个 SpittleRepository 随后又用在 spittles() 方法中,用来获取最新的 spittle 列表。
需要注意的是,我们在 spittles() 方法中给定了一个 Model 作为参 数。这样,spittles() 方法就能将 Repository 中获取到的 Spittle 列表填充到模型中。Model 实际上就是一个 Map(也就是 key-value 对的集合),它会传递给视图,这样数据就能渲染到客户端了。当调用 addAttribute() 方法并且不指定 key 的时候,那么 key 会根据值的对象类型推断确定。在本例中,因为它是一个 List<Spittle>,因此,键将会推断为 spittleList。
spittles() 方法所做的最后一件事是返回 spittles 作为视图的名字,这个视图会渲染模型。
如果你希望显式声明模型的 key 的话,那也尽可以进行指定。例如,下面这个版本的 spittles() 方法与程序清单5.10中的方法作用是一样的:
@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute("splittleList",
spittleRepository.findSpittles(Long.MAX_VALUE, 20)
);
return "spittles"
}
如果你希望使用非 Spring 类型的话,那么可以用 java.util.Map 来代替 Model。下面这个版本的 spittles() 方法与之前的版本在功能上是一样的:
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model) {
model.put("splittleList",
spittleRepository.findSpittles(Long.MAX_VALUE, 20)
);
return "spittles"
}
既然我们现在提到了各种可替代的方案,那下面还有另外一种方式来编写spittles() 方法:
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles() {
return spittleRepository.findSpittles(Long.MAX_VALUE, 20);
}
这个版本与其他的版本有些差别。它并没有返回视图名称,也没有显式地设定模型,这个方法返回的是 Spittle 列表。当处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的 key 会根据其类型推断得出(在本例中,也就是 spittleList)。
而逻辑视图的名称将会根据请求路径推断得出。因为这个方法处理针对 /spittles
的 GET 请求,因此视图的名称将会是 spittles(去掉开头的斜线)。
不管你选择哪种方式来编写 spittles() 方法,所达成的结果都是相同的。模型中会存储一个 Spittle 列表,key 为 spittleList,然后这个列表会发送到名为 spittles 的视图中。按照我们配置 InternalResourceViewResolver 的方式,视图的 JSP 将会是 /WEB-INF/views/spittles.jsp
。
现在,数据已经放到了模型中,在 JSP 中该如何访问它呢?实际上,当视图是 JSP 的时候,模型数据会作为请求属性放到请求(request)之中。因此,在 spittles.jsp 文件中可以使用 JSTL(JavaServer Pages Standard Tag Library)的 <c:forEach>
标签渲染 spittle 列表:
<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>
图 5.3 为显示效果,能够让你对它在 Web 浏览器中是什么样子有个可视化的印象。
尽管 SpittleController 很简单,但是它依然比 HomeController 更进一步了。不过,SpittleController 和 HomeController 都没有处理任何形式的输入。现在,让我们扩展 SpittleController,让它从客户端接受一些输入。