# 5.2.3　传递模型数据到视图中

到现在为止，就编写超级简单的控制器来说，HomeController 已经是一个不错的样例了。但是大多数的控制器并不是这么简单。在 Spittr 应用中，我们需要有一个页面展现最近提交的 Spittle 列表。因此，我们需要一个新的方法来处理这个页面。

首先，需要定义一个数据访问的 Repository。为了实现解耦以及避免陷入数据库访问的细节之中，我们将 Repository 定义为一个接口，并在稍后实现它（第 10 章中）。此时，我们只需要一个能够获取 Spittle 列表的 Repository，如下所示的 SpittleRepository 功能已经足够了：

{% code title="SpittleRepository.java" %}

```java
package spittr.data;

import java.util.List;
import spittr.Spittle;

public interface SpittleRepository {

  List<Spittle> findSpittles(long max, int count);

}
```

{% endcode %}

现在，我们让 Spittle 类尽可能的简单，如下面的程序清单 5.8 所示。它的属性包括消息内容、时间戳以及 Spittle 发布时对应的经纬度。

{% code title="程序清单 5.8 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");
  }
  
}
```

{% endcode %}

就大部分内容来看，Spittle 就是一个基本的 POJO 数据对象 —— 没有什么复杂的。唯一要注意的是，我们使用 Apache Common Lang 包来实现 equals() 和 hashCode() 方法。这些方法除了常规的作用以外，当我们为控制器的处理器方法编写测试时，它们也是有用的。

既然我们说到了测试，那么我们继续讨论这个话题并为新的控制器方法编写测试。如下的程序清单使用 Spring 的 MockMvc 来断言新的处理器方法中你所期望的行为。

{% code title="程序清单 5.9　测试 SpittleController 处理针对 “/spittles” 的 GET 请求" %}

```java
@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;
}
```

{% endcode %}

这个测试首先会创建 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 实现将会满足以上测试的要求。

{% code title="程序清单 5.10　SpittleController：在模型中放入最新的 spittle 列表" %}

```java
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"
  }
  
}
```

{% endcode %}

我们可以看到 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中的方法作用是一样的：

```java
@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() 方法与之前的版本在功能上是一样的：

```java
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model) {
  model.put("splittleList",
    spittleRepository.findSpittles(Long.MAX_VALUE, 20)
  );
  return "spittles"
}
```

既然我们现在提到了各种可替代的方案，那下面还有另外一种方式来编写spittles() 方法：

```java
@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 列表：

```markup
<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，让它从客户端接受一些输入。

![图 5.3　控制器中的 Spittle 模型数据将会作为请求参数，并在 Web 页面上渲染为列表的形式](/files/-LmrKcm4-N5o7dOrzEp2)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v4/di-5-zhang-gou-jian-spring-web-ying-yong-cheng-xu/untitled-4/untitled.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
