天天看點

SpringInAction筆記(五)—— 建構Spring Web應用程式(上)

       Spring MVC架構基于模型-視圖-控制器(Model-View-Controller, MVC)模式實作,能夠幫你建構像Spring架構那樣靈活和松耦合的Web應用程式。

1、Spring MVC起步

1.1、跟蹤Spring MVC的請求

       請求從離開浏覽器開始到擷取響應傳回,它會經曆好多站,在每站都會留下一些資訊同時也會帶上其他資訊。圖展示了請求使用Spring MVC所經曆的所有站點。

SpringInAction筆記(五)—— 建構Spring Web應用程式(上)

(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也需要添加進去。

SpringInAction筆記(五)—— 建構Spring Web應用程式(上)

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/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" /resources/style.css" />" >
  </head>
  <body>
    <h1>Welcome to Spitter</h1>

    <a href="http://blog.163.com/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" /spittles" />" >Spittles</a> | 
    <a href="http://blog.163.com/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" /spitter/register" />" >Register</a>
  </body>
</html>
           

釋出到tomcat,輸入位址:http://localhost:8080/spittr/ ,得到:

SpringInAction筆記(五)—— 建構Spring Web應用程式(上)

注意:如果報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/[email protected]/blog/<c:url value=" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" /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/[email protected]/blog/${more_url}" target="_blank" rel="external nofollow" >Show more</a>
      </c:if>
    </div>
  </body>
</html>
           

輸入http://localhost:8080/spittr/spittles,通路效果如圖:

SpringInAction筆記(五)—— 建構Spring Web應用程式(上)

 測試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中包含預期的内容。