Spring實戰5-基於Spring構建Web應用

主要內容

  • 將web請求映射到Spring控制器html

  • 綁定form參數前端

  • 驗證表單提交的參數java

寫在前面:關於Java Web,首先推薦一篇文章——寫給java web一年左右工做經驗的人,這篇文章的做者用精練的話語勾勒除了各類Java框架的原因和最基本的原理。咱們在學習Spring的過程當中也要切記,不只要知道怎麼作?還要深究背後的思考和權衡。git

對於不少Java程序員來講,他們的主要工做就是開發Web應用,若是你也在作這樣的工做,那麼你必定會了解到構建這類系統所面臨的挑戰,例如狀態管理、工做流和參數驗證等。HTTP協議的無狀態性使得這些任務極具挑戰性。程序員

Spring的web框架用於解決上述提到的問題,基於Model-View-Controller(MVC)模型,Spring MVC能夠幫助開發人員構建靈活易擴展的Web
應用。web

這一章將涉及Spring MVC框架的主要知識,因爲基於註解開發是目前Spring社區的潮流,所以咱們將側重介紹如何使用註解建立控制器,進而處理各種web請求和表單提交。在深刻介紹各個專題以前,首先從一個比較高的層面觀察和理解下Spring MVC的工做原理。spring

5.1 Spring MVC入門

5.1.1 request的處理過程

用戶每次點擊瀏覽器界面的一個按鈕,都發出一個web請求(request)。一個web請求的工做就像一個快遞員,負責將信息從一個地方運送到另外一個地方。數據庫

從web請求離開瀏覽器(1)到返回響應,中間經歷了幾個節點,在每一個節點都進行一些操做用於交換信息。下圖展現了Spring MVC應用中web請求會遇到的幾個節點。apache

web請求通過幾個節點處理而後產生響應信息

請求旅行的第一站是Spring的DispatcherServlet,和大多數Javaweb應用相同,Spring MVC經過一個單獨的前端控制器過濾分發請求。當Web應用委託一個servlet將請求分發給應用的其餘組件時,這個servlert稱爲前端控制器(front controller)。在Spring MVC中,DispatcherServlet就是前端控制器。api

DispatcherServlet的任務是將請求發送給某個Spring控制器。控制器(controller)是Spring應用中處理請求的組件。通常在一個應用中會有多個控制器,DispatcherServlet來決定把請求發給哪一個控制器處理。DispatcherServlet會維護一個或者多個處理器映射(2),用於指出request的下一站——根據請求攜帶的URL作決定。

一旦選好了控制器,DispatcherServlet會把請求發送給指定的控制器(3),控制器中的處理方法負責從請求中取得用戶提交的信息,而後委託給對應的業務邏輯組件(service objects)處理。

控制器的處理結果包含一些須要傳回給用戶或者顯示在瀏覽器中的信息。這些信息存放在模型(model)中,可是直接把原始信息返回給用戶很是低效——最好格式化成用戶友好的格式,例如HTML或者JSON格式。爲了生成HTML格式的文件,須要把這些信息傳給指定的視圖(view),通常而言是JSP。

控制器的最後一個任務就是將數據打包在模型中,而後指定一個視圖的邏輯名稱(由該視圖名稱解析HTML格式的輸出),而後將請求和模型、視圖名稱一塊兒發送回DispatcherServlet4)。

注意,控制器並不負責指定具體的視圖,返回給DispatcherServlet的視圖名稱也不會指定具體的JSP頁面(或者其餘類型的頁面);控制器返回的僅僅是視圖的邏輯名稱,DispatcherServlet用這個名稱查找對應的視圖解析器(5),負責將邏輯名稱轉換成對應的頁面實現,多是JSP也可能不是。

如今DispatcherServlet就已經知道將由哪一個視圖渲染結果,至此一個請求的處理就基本完成了。最後一步就是視圖的實現(6),最經典的是JSP。視圖會使用模型數據填充到視圖實現中,而後將結果放在HTTP響應對象中(7)。

5.1.2 設置Spring MVC

如上一小節的圖展現的,看起來須要填寫不少配置信息。幸運地是,Spring的最新版本提供了不少容易配置的選項,下降了Spring MVC的學習門檻。這裏咱們先簡單配置一個Spring MVC應用,做爲這一章將會不斷完善的例子。

CONFIGURING DISPATCHERSERVLET

DispatcherServlet是Spring MVC的核心,每當應用接受一個HTTP請求,由DispatcherServlet負責將請求分發給應用的其餘組件。

在舊版本中,DispatcherServlet之類的servlet通常在web.xml文件中配置,該文件通常會打包進最後的war包種;可是Spring 3引入了註解,咱們在這一章將展現如何基於註解配置Spring MVC。

既然不適用web.xml文件,你須要在servlet容器中使用Java配置DispatcherServlet,具體的代碼列舉以下:

package org.test.spittr.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() { //根容器
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() { //Spring mvc容器
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() { //DispatcherServlet映射,從"/"開始
        return new String[] { "/" };
    }
}

spitter這個單詞是咱們應用的名稱,SpittrWebAppInitializer類是整個應用的總配置類。

AbstractAnnotationConfigDispatcherServletInitializer這個類負責配置DispatcherServlet、初始化Spring MVC容器和Spring容器。getRootConfigClasses()方法用於獲取Spring應用容器的配置文件,這裏咱們給定預先定義的RootConfig.classgetServletConfigClasses負責獲取Spring MVC應用容器,這裏傳入預先定義好的WebConfig.classgetServletMappings()方法負責指定須要由DispatcherServlet映射的路徑,這裏給定的是"/",意思是由DispatcherServlet處理全部向該應用發起的請求。

A TALE OF TWO APPLICATION CONTEXT

DispatcherServlet啓動時,會建立一個Spring MVC應用容器並開始加載配置文件中定義好的beans。經過getServletConfigClasses()方法,能夠獲取由DispatcherServlet加載的定義在WebConfig.class中的beans。

在Spring Web應用中,還有另外一個Spring應用容器,這個容器由ContextLoaderListener建立。

咱們但願DispatcherServlet僅加載web組件之類的beans,例如controllers(控制器)、view resolvers(視圖解析器)和處理器映射(handler mappings);而但願ContextLoaderListener加載應用中的其餘類型的beans——例如業務邏輯組件、數據庫操做組件等等。

實際上,AbstractAnnotationConfigDispatcherServletInitializer建立了DispatcherServletContextLoaderListenergetServletConfigClasses()返回的配置類定義了Spring MVC應用容器中的beans;getRootConfigClasses()返回的配置類定義了Spring應用根容器中的beans。【書中沒有說的】:Spring MVC容器是根容器的子容器,子容器能夠看到根容器中定義的beans,反之不行。

注意:經過AbstractAnnotationConfigDispatcherServletInitializer配置DispatcherServlet僅僅是傳統的web.xml文件方式的另外一個可選項。儘管你也可使用AbstractAnnotationConfigDispatcherServletInitializer的一個子類引入web.xml文件來配置,但這沒有必要。

這種方式配置DispatcherServlet須要支持Servlert 3.0的容器,例如Apache Tomcat 7或者更高版本的。

ENABLING SPRING MVC

正如能夠經過多種方式配置DispatcherServlet同樣,也能夠經過多種方式啓動Spring MVC特性。原來咱們通常在xml文件中使用<mvc:annotation-driven>元素啓動註解驅動的Spring MVC特性。

這裏咱們仍然使用JavaConfig配置,最簡單的Spring MVC配置類代碼以下:

package org.test.spittr.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
public class WebConfig {
}

@Configuration表示這是Java配置類;@EnableWebMvc註解用於啓動Spring MVC特性。

僅僅這些代碼就能夠啓動Spring MVC了,雖然它換缺了一些必要的組件:

  • 沒有配置視圖解析器。這種狀況下,Spring會使用BeanNameViewResolver,這個視圖解析器經過查找ID與邏輯視圖名稱匹配且實現了View接口的beans。

  • 沒有啓動Component-scanning。

  • DispatcherServlet做爲默認的servlet,將負責處理全部的請求,包括對靜態資源的請求,例如圖片和CSS文件等。

所以,咱們還須要在配置文件中增長一些配置,使得這個應用能夠完成最簡單的功能,代碼以下:

package org.test.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
@ComponentScan("org.test.spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter{
    @Bean
    public ViewResolver viewResolver() { //配置JSP視圖解析器
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        //能夠在JSP頁面中經過${}訪問beans
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable(); //配置靜態文件處理
    }
}

首先,經過@ComponentScan("org.test.spittr.web")註解指定bean的自動發現機制做用的範圍,待會會看到,被@Controller等註解修飾的web的bean將被發現並加載到spring mvc應用容器。這樣就不須要在配置類中顯式定義任何控制器bean了。

而後,你經過@Bean註解添加一個ViewResolverbean,具體來講是InternalResourceViewResolver。後面咱們會專門探討視圖解析器,這裏的三個函數的含義依次是:setPrefix()方法用於設置視圖路徑的前綴;setSuffix()用於設置視圖路徑的後綴,即若是給定一個邏輯視圖名稱——"home",則會被解析成"/WEB-INF/views/home.jsp"; setExposeContextBeansAsAttributes(true)使得能夠在JSP頁面中經過${ }訪問容器中的bean。

最後,WebConfig繼承了WebMvcConfigurerAdapter類,而後覆蓋了其提供的configureDefaultServletHandling()方法,經過調用configer.enable()DispatcherServlet將會把針對靜態資源的請求轉交給servlert容器的default servlet處理。

RootConfig的配置就很是簡單了,惟一須要注意的是,它在設置掃描機制的時候,將以前WebConfig設置過的那個包排除了;也就是說,這兩個掃描機制做用的範圍正交。RootConfig的代碼以下:

package org.test.spittr.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan(basePackages = {"org.test.spittr"},
        excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
public class RootConfig {
}

5.1.3 Spittr應用簡介

這一章要用的例子應用,從Twitter獲取了一些靈感,所以最開始叫Spitter;而後又借鑑了最近比較流行的網站Flickr,所以咱們也把e去掉,最終造成Spittr這個名字。這也有利於區分領域名稱(相似於twitter,這裏用spring實現,所以叫spitter)和應用名稱。

Spittr應用有兩個關鍵的領域概念:spitters(應用的用戶)和spittles(用戶發佈的狀態更新)。在這一章中,將專一於構建該應用的web層,建立控制器和顯示spittles,以及處理用戶註冊的表單。

基礎已經打好了,你已經配置好了DispatcherServlet,啓動了Spring MVC特性等,接下來看看如何編寫Spring MVC控制器。

5.2 編寫簡單的控制器

在Spring MVC應用中,控制器類就是含有被@RequestMapping註解修飾的方法的類,其中該註解用於指出這些方法要處理的請求類型。

咱們從最簡單的請求"/"開始,用於渲染該應用的主頁,HomeController的代碼列舉以下:

package org.test.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)
    public String home() {
        return "home";
    }
}

@Controller是一個模式化的註解,它的做用跟@Component同樣;Component-scanning機制會自動發現該控制器,並在Spring容器中建立對應的bean。

HomeController中的home()方法用於處理http://localhost:8080/這個URL對應的"/"請求,且僅處理GET方法,方法的內容是返回一個邏輯名稱爲"home"的視圖。DispatcherServlet將會讓視圖解析器經過這個邏輯名稱解析出真正的視圖。

根據以前配置的InternalResourceViewResolver,最後解析成/WEB-INF/views/home.jsp,home.jsp的內容列舉以下:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %>
<html>
<head>
    <title>Spittr</title></head>
<body>
    <h1>Welcome to Spittr</h1>
    <a href="<c:url value="/spittles" /> ">Spittles</a>
    <a href="<c:url value="/spitter/register"/> ">Register</a>
</body>
</html>

啓動應用,而後訪問http://localhost:8080/,Spittr應用的主頁以下圖所示:
welcom to spittr

5.2.1 控制器測試

控制器的測試經過Mockito框架進行,首先在pom文件中引入須要的依賴庫:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
</dependency>
<!-- test support -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>${mockito.version}</version>
</dependency><dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
</dependency>

而後,對應的單元測試用例HomeControllerTest的代碼以下所示:

package org.test.spittr.web;

import org.junit.Before;import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
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.*;

public class HomeControllerTest {
    MockMvc mockMvc;

    @Before
    public void setupMock() {
        HomeController controller = new HomeController();
        mockMvc = standaloneSetup(controller).build();
    }

    @Test
    public void testHomePage() throws Exception {
        mockMvc.perform(get("/"))
                .andExpect(view().name("home"));
    }
}

首先stanaloneSetup()方法經過HomeController的實例模擬出一個web服務,而後使用perform執行對應的GET請求,並檢查返回的視圖的名稱。MockMvcBuilders類有兩個靜態接口,表明兩種模擬web服務的方式:獨立測試和集成測試。上面這段代碼是獨立測試,咱們也嘗試了集成測試的方式,最終代碼以下:

package org.test.spittr.web;

import org.junit.Before;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import org.test.spittr.config.RootConfig;
import org.test.spittr.config.WebConfig;

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.*;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextHierarchy({
        @ContextConfiguration(name = "parent", classes = RootConfig.class),
        @ContextConfiguration(name = "child", classes = WebConfig.class)})
public class HomeControllerTest {
    @Autowired
    private WebApplicationContext context;

    MockMvc mockMvc;

    @Before
    public void setupMock() {
        //HomeController controller = new HomeController();
        //mockMvc = standaloneSetup(controller).build();
        mockMvc = webAppContextSetup(context).build();
    }

    @Test
    public void testHomePage() throws Exception {
        mockMvc.perform(get("/"))
                .andExpect(view().name("home"));
    }
}

5.2.2 定義類級別的請求處理

上面一節對以前的HomeController進行了簡單的測試,如今能夠對它進行進一步的完善:將@RequestMapping從修飾函數改爲修飾類,代碼以下:

package org.test.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/")
public class HomeController {
    @RequestMapping(method = RequestMethod.GET)
    public String home() {
        return "home";
    }
}

在新的HomeController中,"/"被移動到類級別的@RequestMapping中,而定義HTTP方法的@RequestMapping仍然用於修飾home()方法。RequestMapping註解能夠接受字符串數組,便可以同時映射多個路徑,所以咱們還能夠按照下面這種方式修改:

@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
    }
}

5.2.3 給視圖傳入模型數據

對於DispatcherServlet傳來的請求,控制器一般不會實現具體的業務邏輯,而是調用業務層的接口,而且將業務層服務返回的數據放在模型對象中返回給DispatcherServlet。

在Spittr應用中,須要一個頁面顯示最近的spittles列表。首先須要定義數據庫存取接口,這裏不須要提供具體實現,只須要用Mokito框架填充模擬測試數據便可。SpittleRepository接口的代碼列舉以下:

package org.test.spittr.data;

import java.util.List;

public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
}

SpittleRepository接口中的findSpittles()方法有兩個參數:max表示要返回的Spittle對象的最大ID;count表示指定須要返回的Spittle對象數量。爲了返回20個最近發表的Spittle對象,則使用List<Spittle> recent = spittleRepository.findSpittle(Long.MAX_VALUE, 20)這行代碼便可。該接口要處理的實體對象是Spittle,所以還須要定義對應的實體類,代碼以下:

package org.test.spittr.data;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import java.util.Date;

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(message, time, null, null);
    }

    public Spittle(String message,Date time, Double latitude, Double longitude) {
        this.id = null;
        this.time = time;
        this.latitude = latitude;
        this.longitude = longitude;
        this.message = message;
    }

    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 obj) {
        return EqualsBuilder.reflectionEquals(this, obj,
                new String[]{"message","latitude", "longitude"});
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this,
                new String[]{"message", "latitude", "longitude"});
    }
}

Spittle對象仍是POJO,並沒什麼複雜的。惟一須要注意的就是,利用Apache Commons Lang庫的接口,用於簡化equals和hashCode方法的實現。參考Apache Commons EqualsBuilder and HashCodeBuilder

首先爲新的控制器接口寫一個測試用例,利用Mockito框架模擬repository對象,並模擬出request請求,代碼以下:

package org.test.spittr.web;

import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.servlet.view.InternalResourceView;
import org.test.spittr.data.Spittle;import org.test.spittr.data.SpittleRepository;import java.util.ArrayList;
import java.util.Date;import java.util.List;

import static org.hamcrest.Matchers.hasItems;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;

public class SpittleControllerTest {
    @Test
    public void shouldShowRecentSpittles() throws Exception {
        //step1 準備測試數據
        List<Spittle> expectedSpittles = createSpittleList(20);
        SpittleRepository mockRepository = mock(SpittleRepository.class);
        when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
                .thenReturn(expectedSpittles);
        SpittleController controller = new SpittleController(mockRepository);
        MockMvc mockMvc = standaloneSetup(controller)
                .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
                .build();

        //step2 and step3
        mockMvc.perform(get("/spittles"))
                .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;
    }
}

單元測試的基本組成是:準備測試數據、調用待測試接口、校驗接口的執行結果。對於shouldShowRecentSpittles()這個用例咱們也能夠這麼分割:首先規定在調用SpittleRepository接口的findSpittles()方法時將返回20個Spittle對象。

這裏選擇獨立測試,跟HomeControllerTest不一樣的地方在於,這裏構建MockMvc對象時還調用了setSingleView()函數,這是爲了防止mock框架從控制器解析view名字。在不少狀況下並無這個必要,可是對於SpittleController控制器來講,視圖名稱和路徑名稱相同,若是使用默認的視圖解析器,則MockMvc會混淆這二者而失敗,報出以下圖所示的錯誤:

default view resovler will confuse the view name and path

在這裏其實能夠隨意設置InternalResourceView的路徑,可是爲了和WebConfig中的配置相同。

經過get方法構造GET請求,訪問"/spittles",並確保返回的視圖名稱是"spittles",返回的model數據中包含spittleList屬性,且對應的值爲咱們以前建立的測試數據。

最後,爲了使用hasItems,須要在pom文件中引入hamcrest庫,代碼以下

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
</dependency>

如今跑單元測試的話,必然會失敗,由於咱們尚未提供SpittleController的對應方法,代碼以下:

package org.test.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 org.test.spittr.data.SpittleRepository;

@Controller
@RequestMapping("/spittles")
public class SpittleController {
    private SpittleRepository spittleRepository;

    @Autowired
    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";
    }
}

Model對象本質上是一個Map,spittles方法負責填充數據,而後跟視圖的邏輯名稱一塊兒回傳給DispatcherServlet。在調用addAttribute方法的時候,若是不指定key字段,則key字段會從value的類型推導出,在這個例子中默認的key字段是spittleList

若是你但願顯式指定key字段,則能夠按照以下方式指定:

@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
    model.addAttribute("spittleList",
            spittleRepository.findSpittles(Long.MAX_VALUE, 20));
    return "spittles";
}

另外,若是你但願儘可能少使用Spring規定的數據類型,則可使用Map代替Model。

還有另外一種spittles方法的實現,以下所示:

@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles() {
    return spittleRepository.findSpittles(Long.MAX_VALUE, 20));
}

這個版本和以前的不一樣,並無返回一個邏輯名稱以及顯式設置Model對象,這個方法直接返回Spittle列表。在這種狀況下,Spring會將返回值直接放入Model對象,並從值類型推導出對應的關鍵字key;而後從路徑推導出視圖邏輯名稱,在這裏是spittles

不管你選擇那種實現,最終都須要一個頁面——spittles.jsp。JSP頁面使用JSTL庫的<c:forEach>標籤獲取model對象中的數據,以下所示:

<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>

儘管SpittleController仍是很簡單,可是它比HomeController複雜了一點,不過,這兩個控制器都沒有實現的一個功能是處理表單輸入。接下來將擴展SpittleController,使其可以處理表單上輸入。

5.3 訪問request輸入

Spring MVC提供了三種方式,可讓客戶端給控制器的handler傳入參數,包括:

  • 查詢參數(Query parameters)

  • 表單參數(Form parameters)

  • 路徑參數(Path parameters)

5.3.1 獲取查詢參數

Spittr應用須要一個頁面顯示spittles列表,目前的SpittleController僅能返回最近的全部spittles,還不能提供根據spittles的生成歷史進行查詢。若是你想提供這個功能,首先要提供用戶一個傳入參數的方法,從而能夠決定返回歷史spittles的那一個子集。

spittles列表是按照ID的生成前後倒序排序的:下一頁spittles的第一條spittle的ID應正好在當前頁的最後一條spittle的ID後面。所以,爲了顯示下一頁spttles,應該可以傳入僅僅小於當前頁最後一條spittleID的參數;而且提供設置每頁返回幾個spittles的參數count。

  • before參數,表明某個Spittle的ID,包含該ID的spittles集合中全部的spittles都在當前頁的spittles以前發佈;

  • count參數,表明但願返回結果中包含多少條spittles。

咱們將改造5.2.3小節實現的spittles()方法,來處理上述兩個參數。首先編寫測試用例:

@Test
public void shouldShowRecentSpittles_NORMAL() 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())));
}

這個測試用例的關鍵在於:爲請求"/spittles"傳入兩個參數,max和count。這個測試用例能夠測試提供參數的狀況,兩個測試用例都應該提供,這樣能夠覆蓋到全部測試條件。改造後的spittles方法列舉以下:

@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(
        @RequestParam("max") long max,
        @RequestParam("count") int count) {
    return spittleRepository.findSpittles(max, count);
}

若是SpittleController的handle方法須要默認處理同時處理兩種狀況:提供了max和count參數,或者沒有提供的狀況,代碼以下:

@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(
        @RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
        @RequestParam(value = "count", defaultValue = "20") int count) {
    return spittleRepository.findSpittles(max, count);
}

其中MAX_LONG_AS_STRING是Long的最大值的字符串形式,定義爲:private static final String MAX_LONG_AS_STRING = Long.MAX_VALUE + "";,默認值都給定字符串形式,不過一旦須要綁定到參數上時,就會自動轉爲合適的格式。

5.3.2 經過路徑參數獲取輸入

假設Spittr應用應該支持經過指定ID顯示對應的Spittle,可使用@RequestParam給控制器的處理方法傳入參數ID,以下所示:

@RequestMapping(value = "/show", method = RequestMethod.GET)
public String showSpittle(
        @RequestParam("spittle_id") long spittleId,
        Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

這個方法將處理相似/spittles/show?spittle_id=12345的請求,儘管這能夠工做,可是從基於資源管理的角度並不理想。理想狀況下,某個指定的資源應該能夠經過路徑指定,而不是經過查詢參數指定,所以GET請求最好是這種形式:/spittles/12345

首先編寫一個測試用例,代碼以下:

@Test
public void testSpittle() throws Exception {
    Spittle expectedSpittle = new Spittle("Hello", new Date());
    SpittleRepository mockRepository = mock(SpittleRepository.class);
    when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(get("/spittles/12345"))
            .andExpect(view().name("spittle"))
            .andExpect(model().attributeExists("spittle"))
            .andExpect(model().attribute("spittle", expectedSpittle));
}

該測試用例首先模擬一個repository、控制器和MockMvc對象,跟以前的幾個測試用例相同。不一樣之處在於這裏構造的GET請求——/spittles/12345,並但願返回的視圖邏輯名稱是spittle,返回的模型對象中包含關鍵字spittle,且與該key對應的值爲咱們建立的測試數據。

爲了實現路徑參數,Spring MVC在@RequestMapping註解中提供佔位符機制,並在參數列表中經過@PathVariable("spittleId")獲取路徑參數,完整的處理方法列舉以下:

@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String showSpittle(
        @PathVariable("spittleId") long spittleId,
        Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

@PathVariable註解的參數應該和@RequestMapping註解中的佔位符名稱徹底相同;若是函數參數也和佔位符名稱相同,則能夠省略@PathVariable註解的參數,代碼以下所示:

@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String showSpittle(
        @PathVariable long spittleId,
        Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

這麼寫確實可使得代碼更加簡單,不過須要注意:若是要修改函數參數名稱,則要同時修改路徑參數的佔位符名稱。

5.4 處理表單

Web應用一般不只僅是給用戶顯示數據,也接受用戶的表單輸入,最典型的例子就是帳號註冊頁面——須要用戶填入相關信息,應用程序按照這些信息爲用戶建立一個帳戶。

關於表單的處理有兩個方面須要考慮:顯示錶單內容和處理用戶提交的表單數據。在Spittr應用中,須要提供一個表單供新用戶註冊使用;須要一個SpitterController控制器顯示註冊信息。

package org.test.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/spitter")
public class SpitterController {
    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String showRegistrationForm() {
        return "registerForm";
    }
}

修飾showRegistrationForm()方法的@RequestMapping(value = "/register", method = RequestMethod.GET)註解,和類級別的註解一塊兒,代表該方法須要處理相似"/spitter/register"的GET請求。這個方法很是簡單,沒有輸入,且僅僅返回一個邏輯名稱——"registerForm"。

即便showRegistrationForm()方法很是簡單,也應該寫個單元測試,代碼以下所示:

@Test
public void shouldShowRegistrationForm() throws Exception {
    SpitterController controller = new SpitterController();
    MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(get("/spitter/register"))
            .andExpect(view().name("registerForm"));
}

爲了接受用戶的輸入,須要提供一個JSP頁面——registerForm.jsp,該頁面的代碼以下所示:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spittr</title>
</head>
<body>
  <h1>Register</h1>
  <form method="POST">
    First Name: <input type="text" name="firstName" /><br/>
    Last Name: <input type="text" name="lastName"/><br/>
    Username: <input type="text" name="username"/><br/>
    Password: <input type="password" name="password" /><br/>
    <input type="submit" value="Register" />
  </form>
</body>
</html>

上述JSP頁面在瀏覽器中渲染圖以下所示:

註冊頁面

由於<form>標籤並無設置action參數,所以,當用戶單擊submit按鈕的時候,將向後臺發出/spitter/register的POST請求。這就須要咱們爲SpitterController編寫對應的處理方法。

5.4.1 編寫表單控制器

在處理來自注冊表單的POST請求時,控制器須要接收表單數據,而後構造Spitter對象,並保存在數據庫中。爲了不重複提交,應該重定向到另外一個頁面——用戶信息頁。

按照慣例,首先編寫測試用例,以下所示:

@Test
public void shouldProcessRegistration() throws Exception {
    SpitterRepository mockRepository = mock(SpitterRepository.class);
    Spitter unsaved = new Spitter("Jack", "Bauer", "jbauer", "24hours");
    Spitter saved = new Spitter(24L, "Jack", "Bauer", "jbauer", "24hours");
    when(mockRepository.save(unsaved)).thenReturn(saved);

    SpitterController controller = new SpitterController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(post("/spitter/register")
            .param("firstName", "Jack")
            .param("lastName", "Bauer")
            .param("username", "jbauer")
            .param("password", "24hours"))
            .andExpect(redirectedUrl("/spitter/jbauer"));

    //Verified save(unsaved) is called atleast once
    verify(mockRepository, atLeastOnce()).save(unsaved);
}

顯然,這個測試比以前驗證顯示註冊頁面的測試更加豐富。首先設置好SpitterRepository對象、控制器和MockMvc對象,而後構建一個POST請求——/spitter/register,且該請求會攜帶四個參數,用於模擬submit的提交動做。

在處理POST請求的最後通常須要利用重定向到一個新的頁面,以防瀏覽器刷新引來的重複提交。在這個例子中咱們重定向到/spitter/jbaure,即新添加的用戶的我的信息頁面。

最後,該測試用例還須要驗證模擬對象mockRepository確實用於保存表單提交的數據了,即save()方法之上調用了一次。

SpitterController中添加處理表單的方法,代碼以下:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(Spitter spitter) {
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

shouldShowRegistrationForm()這個方法還在,新加的處理方法processRegistration()以Spitter對象爲參數,Spring利用POST請求所攜帶的參數初始化Spitter對象。

如今執行以前的測試用例,發現一個錯誤以下所示:

argument are different

我分析了這個錯誤,緣由是測試用例的寫法有問題:verify(mockRepository, atLeastOnce()).save(unsaved);這行代碼表示,但願調用至少保存unsave這個對象一次,而實際上在控制器中執行save的時候,參數對象的ID是另外一個——根據參數新建立的。回顧咱們寫這行代碼的初衷:確保save方法至少被調用一次,而保存哪一個對象則無所謂,所以,這行語句改爲verify(mockRepository, atLeastOnce());後,再次執行測試用例就能夠經過了。

注意:不管使用哪一個框架,請儘可能不要使用verify,也就是傳說中的Mock模式,那是把代碼拉入泥潭的開始。參見你應該更新的Java知識之經常使用程序庫

InternalResourceViewResolver看到這個函數返回的重定向URL是以view標誌開頭,就知道須要把該URL當作重定向URL處理,而不是按照視圖邏輯名稱處理。在這個例子中,頁面將被重定向至用戶的我的信息頁面。所以,咱們還須要給SpitterController添加一個處理方法,用於顯示我的信息,showSpitterProfile()方法代碼以下:

@RequestMapping(value = "/{username}", method = RequestMethod.GET)
public String showSpitterProfile(
    @PathVariable String username, Model model) {
    Spitter spitter = spitterRepository.findByUsername(username);
    model.addAttribute(spitter);
    return "profile";
}

showSpitterProfile()方法根據username從SpitterRepository中查詢Spitter對象,而後將該對象存放在model對象中,並返回視圖的邏輯名稱profile

profile.jsp的頁面代碼以下所示:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Your Profile</title>
</head>
<body>
    <h1>Your Profile</h1>
    <c:out value="${spitter.username}"/><br/>
    <c:out value="${spitter.firstName}"/><br/>
    <c:out value="${spitter.lastName}" /><br/>
</body>
</html>

上述代碼的渲染圖以下圖所示:

Your Profile

5.4.2 表單驗證

若是用戶忘記輸入username或者password就點了提交,則可能建立一個這兩個字段爲空字符串的Spitter對象。往小了說,這是醜陋的開發習慣,往大了說這是會應發安全問題,由於用戶能夠經過提交一個空的表單來登陸系統。

綜上所述,須要對用戶的輸入進行有效性驗證,一種驗證方法是爲processRegistration()方法添加校驗輸入參數的代碼,由於這個函數自己很是簡單,參數也很少,所以在開頭加入一些If判斷語句還能夠接受。

除了使用這種方法,換能夠利用Spring提供的Java驗證支持(a.k.a JSR-303)。從Spring 3.0開始,Spring支持在Spring MVC項目中使用Java Validation API。

首先須要在pom文件中添加依賴:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
</dependency>

而後就可使用各種具體的註解,進行參數驗證了,以Spitter類的實現爲例:

package org.test.spittr.data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Spitter {
    private Long id;

    @NotNull
    @Size(min = 5, max = 16)
    private String username;

    @NotNull
    @Size(min = 5, max = 25)
    private String password;

    @NotNull
    @Size(min = 2, max = 30)
    private String firstName;

    @NotNull
    @Size(min = 2, max = 30)
    private String lastName;

    ....
}

@NotNull註解表示被它修飾的字段不能爲空;@Size字段用於限制指定字段的長度範圍。在Spittr應用的含義是:用戶必須填寫表單中的全部字段,而且知足必定的長度限制,才能夠註冊成功。

除了上述兩個註解,Java Validation API提供了不少不一樣功能的註解,都定義在javax.validation.constraints包種,下表列舉出這些註解:

Java Validation API列表

Java Validation API列表(續)

Spittr類的定義中規定驗證條件後,須要在控制器的處理方法中應用驗證條件。

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(
        @Valid Spitter spitter,
        Errors errors) {
    if (errors.hasErrors()) {
        return "registerForm";
    }
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

若是用戶輸入的參數有誤,則返回registerForm這個邏輯名稱,瀏覽器將返回到表單填寫頁面,以便用戶從新輸入。固然,爲了更好的用戶體驗,還須要提示用戶具體哪一個字段寫錯了,應該怎麼改;最好是在用戶填寫以前就作出提示,這就須要前端工程師作不少工做了。

5.5 總結

這一章比較適合Spring MVC的入門學習資料。涵蓋了Spring MVC處理web請求的處理過程、如何寫簡單的控制器和控制器方法來處理Http請求、如何使用mockito框架測試控制器方法。

基於Spring MVC的應用有三種方式讀取數據:查詢參數、路徑參數和表單輸入。本章用兩節介紹了這些內容,並給出了相似錯誤處理和參數驗證等關鍵知識點。

相關文章
相關標籤/搜索