5.2.3 传递模型数据到视图中
到现在为止,就编写超级简单的控制器来说,HomeController 已经是一个不错的样例了。但是大多数的控制器并不是这么简单。在 Spittr 应用中,我们需要有一个页面展现最近提交的 Spittle 列表。因此,我们需要一个新的方法来处理这个页面。
首先,需要定义一个数据访问的 Repository。为了实现解耦以及避免陷入数据库访问的细节之中,我们将 Repository 定义为一个接口,并在稍后实现它(第 10 章中)。此时,我们只需要一个能够获取 Spittle 列表的 Repository,如下所示的 SpittleRepository 功能已经足够了:
SpittleRepository.java
1
package spittr.data;
2
3
import java.util.List;
4
import spittr.Spittle;
5
6
public interface SpittleRepository {
7
8
List<Spittle> findSpittles(long max, int count);
9
10
}
Copied!
现在,我们让 Spittle 类尽可能的简单,如下面的程序清单 5.8 所示。它的属性包括消息内容、时间戳以及 Spittle 发布时对应的经纬度。
程序清单 5.8 Spittle 类:包含消息内容、时间戳和位置信息
1
package spittr;
2
3
import java.util.Date;
4
5
import org.apache.commons.lang3.builder.EqualsBuilder;
6
import org.apache.commons.lang3.builder.HashCodeBuilder;
7
8
public class Spittle {
9
10
private final Long id;
11
private final String message;
12
private final Date time;
13
private Double latitude;
14
private Double longitude;
15
16
public Spittle(String message, Date time) {
17
this(null, message, time, null, null);
18
}
19
20
public Spittle(Long id, String message, Date time, Double longitude, Double latitude) {
21
this.id = id;
22
this.message = message;
23
this.time = time;
24
this.longitude = longitude;
25
this.latitude = latitude;
26
}
27
28
public long getId() {
29
return id;
30
}
31
32
public String getMessage() {
33
return message;
34
}
35
36
public Date getTime() {
37
return time;
38
}
39
40
public Double getLongitude() {
41
return longitude;
42
}
43
44
public Double getLatitude() {
45
return latitude;
46
}
47
48
@Override
49
public boolean equals(Object that) {
50
return EqualsBuilder.reflectionEquals(this, that, "id", "time");
51
}
52
53
@Override
54
public int hashCode() {
55
return HashCodeBuilder.reflectionHashCode(this, "id", "time");
56
}
57
58
}
Copied!
就大部分内容来看,Spittle 就是一个基本的 POJO 数据对象 —— 没有什么复杂的。唯一要注意的是,我们使用 Apache Common Lang 包来实现 equals() 和 hashCode() 方法。这些方法除了常规的作用以外,当我们为控制器的处理器方法编写测试时,它们也是有用的。
既然我们说到了测试,那么我们继续讨论这个话题并为新的控制器方法编写测试。如下的程序清单使用 Spring 的 MockMvc 来断言新的处理器方法中你所期望的行为。
程序清单 5.9 测试 SpittleController 处理针对 “/spittles” 的 GET 请求
1
@Test
2
public void shouldShowPagedSpittles() throws Exception {
3
List<Spittle> expectedSpittles = createSpittleList(50);
4
SpittleRepository mockRepository = mock(SpittleRepository.class);
5
when(mockRepository.findSpittles(238900, 50))
6
.thenReturn(expectedSpittles);
7
8
SpittleController controller = new SpittleController(mockRepository);
9
MockMvc mockMvc = standaloneSetup(controller)
10
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
11
.build();
12
13
mockMvc.perform(get("/spittles?max=238900&count=50"))
14
.andExpect(view().name("spittles"))
15
.andExpect(model().attributeExists("spittleList"))
16
.andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray())));
17
}
18
19
...
20
21
private List<Spittle> createSpittleList(int count) {
22
List<Spittle> spittles = new ArrayList<Spittle>();
23
for (int i=0; i < count; i++) {
24
spittles.add(new Spittle("Spittle " + i, new Date()));
25
}
26
return spittles;
27
}
Copied!
这个测试首先会创建 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 列表
1
package spittr.web;
2
3
import java.util.List;
4
5
import org.springframework.beans.factory.annotation.Autowired;
6
import org.springframework.stereotype.Controller;
7
import org.springframework.web.bind.annotation.RequestMapping;
8
import org.springframework.web.bind.annotation.RequestMethod;
9
10
import spittr.Spittle;
11
import spittr.data.SpittleRepository;
12
13
@Controller
14
@RequestMapping("/spittles")
15
public class SpittleController {
16
17
private SpittleRepository spittleRepository;
18
19
@Autowired
20
public SpittleController(SpittleRepository spittleRepository) {
21
this.spittleRepository = spittleRepository;
22
}
23
24
@RequestMapping(method=RequestMethod.GET)
25
public String spittles(Model model) {
26
model.addAttribute(
27
spittleRepository.findSpittles(Long.MAX_VALUE, 20)
28
);
29
return "spittles"
30
}
31
32
}
Copied!
我们可以看到 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中的方法作用是一样的:
1
@RequestMapping(method=RequestMethod.GET)
2
public String spittles(Model model) {
3
model.addAttribute("splittleList",
4
spittleRepository.findSpittles(Long.MAX_VALUE, 20)
5
);
6
return "spittles"
7
}
Copied!
如果你希望使用非 Spring 类型的话,那么可以用 java.util.Map 来代替 Model。下面这个版本的 spittles() 方法与之前的版本在功能上是一样的:
1
@RequestMapping(method=RequestMethod.GET)
2
public String spittles(Map model) {
3
model.put("splittleList",
4
spittleRepository.findSpittles(Long.MAX_VALUE, 20)
5
);
6
return "spittles"
7
}
Copied!
既然我们现在提到了各种可替代的方案,那下面还有另外一种方式来编写spittles() 方法:
1
@RequestMapping(method=RequestMethod.GET)
2
public List<Spittle> spittles() {
3
return spittleRepository.findSpittles(Long.MAX_VALUE, 20);
4
}
Copied!
这个版本与其他的版本有些差别。它并没有返回视图名称,也没有显式地设定模型,这个方法返回的是 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 列表:
1
<c:forEach items="${spittleList}" var="spittle" >
2
<li id="spittle_<c:out value="spittle.id"/>">
3
<div class="spittleMessage">
4
<c:out value="${spittle.message}" />
5
</div>
6
<div>
7
<span class="spittleTime">
8
<c:out value="${spittle.time}" />
9
</span>
10
<span class="spittleLocation">(
11
<c:out value="${spittle.latitude}" />,
12
<c:out value="${spittle.longitude}" />)
13
</span>
14
</div>
15
</li>
16
</c:forEach>
Copied!
图 5.3 为显示效果,能够让你对它在 Web 浏览器中是什么样子有个可视化的印象。
尽管 SpittleController 很简单,但是它依然比 HomeController 更进一步了。不过,SpittleController 和 HomeController 都没有处理任何形式的输入。现在,让我们扩展 SpittleController,让它从客户端接受一些输入。
图 5.3 控制器中的 Spittle 模型数据将会作为请求参数,并在 Web 页面上渲染为列表的形式
Last modified 2yr ago
Copy link