5.2.3 传递模型数据到视图中

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

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

SpittleRepository.java
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,让它从客户端接受一些输入。

Last updated