Spring之旅第八站:Spring MVC Spittr舞臺的搭建、基本的控制器、請求的輸入、表單驗證、測試(重點)

構建Spring Web應用

說明

若是你有幸能看到。css

  • 一、本文參考了《Spring 實戰》重點內容,參考了GitHub上的代碼
  • 二、本文只爲記錄做爲之後參考,要想真正領悟Spring的強大,請看原書。
  • 三、在一次佩服老外,國外翻譯過來的書,在GiuHub上大都有實例。看書的時候,跟着敲一遍,效果很好。
  • 四、代碼和筆記在這裏GitHub,對你有幫助的話,歡迎點贊。
  • 五、每一個人的學習方式不同,找到合適本身的就行。2018,加油。
  • 六、Java 8 In Action 的做者Mario Fusco
  • 七、Spring In Action 、Spring Boot In Action的做者Craig Walls
  • 八、知其然,也要知其因此然。

談一些我的感覺html

  • 一、趕快學習Spring吧,Spring MVC 、Spring Boot 、微服務。
  • 二、重點中的重點,學習JDK 8 Lambda,Stream,Spring 5 最低要求JDK1.8.
  • 三、還有Netty、放棄SH吧,否則你會落伍的。
  • 四、多看一些國外翻譯過來的書,例如 Xxx In Action 系列。權威指南系列。用Kindle~
  • 五、寫代碼以前先寫測試,這就是老外不一樣之處。學到了不少技巧。

系統面臨的挑戰:狀態管理、工做流、以及驗證都是須要解決的重要特性。HTTP協議的無狀態決定了這些問題都不是那麼容易解決。前端

Spring的Web框架就是爲了幫你解決這些關注點而設計的。Spring MVC基於模型-視圖-控制器(Model-View-Controller MVC)模式實現的,他可以幫你構建向Spring框架那樣靈活和鬆耦合的Web應用程序。java

在本章中,將會介紹Spring MVC Web框架,並使用新的Spring MVC註解來構建處理各類Web請求、參數、和表單輸入的控制器。git

5.1 Spring MVC起步

Spring將請求在調度Servlet、處理器映射(Handler Mappering)、控制器以及視圖解析器(View resolver)之間移動,每個Spring MVC中的組件都有特定的目的,而且也沒那麼複雜。github

讓咱們看一下,請求是如何從客戶端發起,通過Spring MVC中的組件,最終返回到客戶端web

5.1.1 跟蹤Spring MVC

每當用戶在Web瀏覽器中點擊連接或提交表單的時候,請求就開始工做了。請求是一個十分繁忙的傢伙,從離開瀏覽器開始到獲取響應返回,它會經歷不少站,在每站都會留下一些信息,同時也會帶上一些信息。正則表達式

Spring工做流程描述原文在這裏spring

    1. 用戶向服務器發送請求,請求被Spring 前端控制Servelt DispatcherServlet捕獲;
    1. DispatcherServlet對請求URL進行解析,獲得請求資源標識符(URI)。而後根據該URI,調用HandlerMapping得到該Handler配置的全部相關的對象(包括Handler對象以及Handler對象對應的攔截器),最後以HandlerExecutionChain對象的形式返回;
    1. DispatcherServlet 根據得到的Handler,選擇一個合適的HandlerAdapter。(附註:若是成功得到HandlerAdapter後,此時將開始執行攔截器的preHandler(...)方法)
    1. 提取Request中的模型數據,填充Handler入參,開始執行Handler(Controller)。 在填充Handler的入參過程當中,根據你的配置,Spring將幫你作一些額外的工做:
    • HttpMessageConveter: 將請求消息(如Json、xml等數據)轉換成一個對象,將對象轉換爲指定的響應信息
    • 數據轉換:對請求消息進行數據轉換。如String轉換成Integer、Double等
    • 數據根式化:對請求消息進行數據格式化。 如將字符串轉換成格式化數字或格式化日期等
    • 數據驗證: 驗證數據的有效性(長度、格式等),驗證結果存儲到BindingResult或Error中
    1. Handler執行完成後,向DispatcherServlet 返回一個ModelAndView對象;
    1. 根據返回的ModelAndView,選擇一個適合的ViewResolver(必須是已經註冊到Spring容器中的ViewResolver)返回給DispatcherServlet ;
    1. ViewResolver 結合Model和View,來渲染視圖
    1. 將渲染結果返回給客戶端。

圖片參考這裏sql

Spring工做流程描述

  • 爲何Spring只使用一個Servlet(DispatcherServlet)來處理全部請求?
  • 詳細見J2EE設計模式-前端控制模式
  • Spring爲何要結合使用HandlerMapping以及HandlerAdapter來處理Handler?
  • 符合面向對象中的單一職責原則,代碼架構清晰,便於維護,最重要的是代碼可複用性高。如HandlerAdapter可能會被用於處理多種Handler。

一、請求旅程的第一站是Spring的DispatcherServlet。與大多數基於Java的Web框架同樣,Spring MVC全部的請求都會經過一個前端控制器(front contrller)Servlet.前端控制器是經常使用Web應用程序模式。在這裏一個單實例的Servlet將請求委託給應用的其餘組件來執行實際的處理。在Spring MVC中,DisPatcherServlet就是前端控制器。

二、DisPactcher的任務是將請求發送Spring MVC控制器(controller).控制器是一個用於處理請求的Spring組件。在典型的應用中可能會有多個控制器,DispatcherServlet須要知道應該將請求發送給那個哪一個控制器。因此Dispactcher以會查詢一個或 多個處理器映射(Handler mapping),來肯定請求的下一站在哪裏。處理映射器根據請求攜帶的 URL信息來進行決策。

三、一旦選擇了合適的控制器,DispatcherServlet會將請求發送給選中的控制器。到了控制器,請求會卸下其負載(用戶提交的信息)並耐心等待控制器處理這些信息。(實際上,設計良好的控制器 自己只是處理不多,甚至不處理工做,而是將業務邏輯委託給一個或多個服務器對象進行處理)

四、控制器在完成處理邏輯後,一般會產生一些信息。這些 信息須要返回給 用戶,並在瀏覽器上顯示。這些信息被稱爲模型(Model),不過僅僅給用戶返回原始的信息是不夠的----這些信息須要以用戶友好的方式進行格式化,通常會是HTML。因此,信息須要發送一個視圖(View),一般會是JSP。

五、 控制器作的最後一件事就是將模型打包,而且表示出用於渲染輸出的視圖名。它接下來會將請求連同模型和視圖發送回DispatcherServlet。

六、這樣,控制器就不會與特定的視圖相耦合*傳遞給控制器的視圖名並不直接表示某個特定的jsp。實際上,它甚至並不能肯定視圖就是JSP。相反,它僅僅傳遞了一個邏輯名稱,這個名字將會用來查找產生結果的真正視圖。DispatcherServlet將會使用視圖解析器(View resolver),來將邏輯視圖名稱匹配爲一個特定的視圖實現,他可能也可能不是JSP

七、雖然DispatcherServlet已經知道了哪一個駛入渲染結果、那請求的任務基本上也就完成了,它的最後一站是試圖的實現。在這裏它交付給模型數據。請求的任務就結束了。視圖將使用模型數據渲染輸出。這個輸出經過響應對象傳遞給客戶端(不會像聽上去那樣硬編碼)

能夠看到,請求要通過不少步驟,最終才能造成返回給客戶端的響應,大多數的 步驟都是在Spirng框架內部完成的。

5.1.2 搭建Spring MVC

藉助於最近幾個Spring新特性的功能加強,開始使用SpringMVC變得很是簡單了。使用最簡單的方式配置Spring MVC;所要實現的功能僅限於運行咱們所建立的控制器。

配置DisPatcherServlet

DispatcherServlet是Spirng MVC的核心,在這裏請求會第一次接觸到框架,它要負責將請求路由到其餘組件之中。

按照傳統的方式,像DispatcherServlet這樣的Servlet會配置在web.xml中。這個文件會放到應用的war包中。固然這是配置DispatcherServlet方法之一。藉助於Servlet 3規範和Spring 3.1 的功能加強,這種方式已經不是惟一的方案來。

咱們會使用Java將DispatcherServlet配置在Servlet容器中。而不會在使用web.xml文件

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected String[] getServletMappings() {             //將DispatcherServlet映射到「/」
        return new String[]{"/"};
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?> [] {RootConfig.class};
    }

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

咱們只須要知道擴展AbstractAnnotationConfigDispatcherServletInitializer的任意類都會自動的配置Dispatcherservlet和Spring應用上下文,Spirng的應用上下文會位於應用程序的Servlet上下文之中

在Servlet3.0環境中,容器會在類路徑中 查找實現javax.servlet.ServletContainerInitialzer接口的類,若是能發現的話,就會用它來配置Servlet容器。

Spring提供了這個接口的實現名爲SpringServletContainnerInitialzer,這個類反過來又會查找實現WebApplicationInitialzer的類,並將配置的任務交給他們來完成。Spring 3.2引入了一個遍歷的WebApplicationInitialzer基礎實現也就是AbstractAnnotationConfigDispatcherServletInitializer由於咱們的Spittr-WebApplicationInitialzer擴展了AbstractAnnotationConfigDispatcherServletInitializer,(同時也就實現了WebApplicationInitialzer),所以當部署Servlet3.0容器的時候,容器會自動發現它,並用它來配置Servlet上下文

第一個方法getServletMappings(),它會將一個或多個路徑映射到DispatcherServlet上,在本示例中,它映射的是「/」,表示它是應用默認的Servlet,它會處理應用的全部請求。

爲了理解其餘兩個方法,咱們首先須要理解DispatcherServlet和一個Servlet監聽器(也就是ContextLoaderListener)的關係。

DispatcherServlet啓動的時候,它會建立應用上下文,並加載配置文件或配置類中聲明的bean。在上面那個程序中的getServletConfigClasses()方法中,咱們要求DispatcherServlet加載應用上下文時,使用定義在WebConfig配置類(使用Java配置)中的bean

但在Spring Web應用中,一般還會有另一個應用上下文。另外這個就是由ContextLoaderListener建立.

咱們但願DispatcherServlet加載包含Web組件的bean,如控制器,視圖解析器,以及處理器映射,而ContextLoaderListener要加載應用中的其餘bean。這些bean一般 是驅動應用後端的中間層和數據層組件。

實際上AbstractAnnotationConfigDispatcherServletInitializer會同時建立DispatcherServletContextLoaderListenergetServletConfigClasses()方法會返回帶有@Configuration註解的類將會用來定義DispatcherSerle應用上下文中的bean,getRootConfigClasses()會返回帶有@Configuration註解的類將會用來配置ContextLoaderListener建立的應用上下文。

若是有必要兩個能夠同時存在,wex.xml和 AbstractAnnotationConfigDispatcherServletInitializer,但其實沒有必要。

若是按照這種方式配置DispatcherServlet,而不是使用Web.xml的話,那麼惟一的問題在於它能部署到支持Servlet3.0的服務器上才能夠正常工做,如Tomcat7或更高版本,Servlet3.0規範在2009年12月份就發佈了,

若是沒有支持Servlet3.0,那別無選擇了,只能使用web.xml配置類。

啓用Spring MVC

咱們有多種方式來啓動DispatcherServlet,與之相似,啓用Spring MVC組件的方式也不止一種,之前Spring是XMl進行配置的,你能夠選擇<mvc:annotation-driver>啓用註解驅動的Spring MVC。

在第七章的時候會介紹<mvc:annotaion-driver>,如今會讓Spring MVC搭建的過程儘量簡單,並基於Java進行配置。

咱們所能建立最簡單的Spring MVC配置就是一個帶有@EnableWebMvc註解的類

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

@Configuration
@EnableWebMvc
public class WebConfig {
}

這能夠運行起來,它的確可以啓用Spring MVC,但還有很多問題要解決。

一、沒有配置視圖解析器,若是這樣的話,Spring默認會使用BeanNameView-Resolver,這個視圖解析器會查找ID與視圖名稱匹配的bean,而且查找的bean要實現View接口,它以這樣的方式來解析視圖。

二、沒有啓用組件掃描。這樣的結果就是,Spirng只能找到顯示聲明在配置類中的控制器。

三、這樣配置的話,DispatcherServlet會映射爲默認的Servlet,因此他會處理全部的請求,包括對靜態資源的請求,如圖片 和樣式表(在大多數狀況下,這可能並非你想要的結果)。

所以咱們須要在WebConfig這個最小的Spring MVC配置上再加一些內容,從而讓他變得真正實用。

@Configuration
@EnableWebMvc                           //啓用Spring MVC
@ComponentScan("com.guo.spittr.web")    //啓用組件掃描
public class WebConfig extends WebMvcConfigurerAdapter {
    @Bean
    public ViewResolver viewResolver () {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        //配置JSP視圖解析器
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    //咱們要求DispatcherServlet將靜態資源的請求轉發到Servlet容器中默認的Servlet上,
    //而不是使用DispatcherServlet原本來處理此類請求。
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        //配置靜態資源的處理
        configurer.enable();
    }
}

第一件須要注意的是WebConfig如今添加了@ComponentScan註解,此時將會掃描com.guo.spittr.web包來查找組件。稍後你會看到,咱們編寫的控制器將會帶有@Controller註解,這會使其成爲組件掃描時的候選bean。所以,咱們不須要在配置類中顯示聲明任何的控制器。

接下來,咱們添加了一個ViewResolver bean,更具體的將是InternalResourceViewResolver。將會在第6章更爲詳細的討論視圖解析器。咱們只須要知道他會去查找jsp文件,在查找的時候,它會在視圖名稱上加一個特定的前綴和後綴。(例如:名爲home的視圖會被解析爲/WEB-INF/views/home.jsp)

最後新的WebConfig類還擴展裏WebMvcConfigurerAdapter並重寫了其configureDefaultServletHandling()方法,經過調用DefaultServletHandlerConfigurer的enable()方法,咱們要求DispatcherServlet將靜態資源的請求轉發到Servlet容器中默認的Servlet上,而不是使用DispatcherServlet原本來處理此類請求。

WebConfig已經就緒,那麼RootConfig呢?由於本章聚焦於Web開發,而Web相關的配置經過DisPatcherServlet建立的應用上下文都已經配好了,所以如今的RootConfig相對很簡單:

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;
/**
 * Created by guo on 23/2/2018.
 */
@Configuration
@ComponentScan(basePackages = {"com.guo.spittr"},
    excludeFilters = {
        @Filter(type = FilterType.ANNOTATION,value = EnableWebMvc.class)})
public class RootConfig {

}

惟一須要注意的是RootConfig使用了@ComponentScan註解,這樣的話,咱們就有不少機會用非Web的組件來完善RootConfig。

5.1.3 Spittr應用簡介

爲了實如今線社交的功能,咱們將要構造一個簡單的微博(microblogging)應用,在不少方面,咱們所構建的應用於最先的微博應用Twitter很相似,在這個過程當中,咱們會添加一些小的變化。固然咱們使用Spirng技術來構建這個應用。

由於從Twitter借鑑了靈感並經過Spring來進行實現,因此它就有了一個名字:Spitter。

Spittr應用有兩個基本的領域概念:Spitter(應用的用戶)和Spittle(用戶發佈的簡短狀態更新)。當咱們在書中完善Spittr應用的功能時,將會介紹這兩個概念。在本章中,咱們會構建應用的Web層,建立展示Spittle的控制器以及處理用戶註冊爲Spitter的表單。

舞臺已經搭建完成了,咱們已經配置了DispatcherServlet,啓用了基本的Spring MVC組件,並肯定了目標應用。讓咱們進入本章的核心內容:使用Spring MVC 控制器處理Web請求。

5.2 編寫 基本的控制器

在SpringMVC中,控制器只是在方法上添加了@RequestMapping註解的類,這個註解聲明瞭他們所要處理的請求。

開始的時候,咱們儘量簡單,假設控制器類要處理對/的請求,並對渲染應用的首頁。

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

/**
 * Created by guo on 24/2/2018.
 * 首頁控制器
 */
@Controller
public class HomeController {
    @RequestMapping(value = "/",method = RequestMethod.GET)           //處理對「/」的Get請求
    public String home() {
        return "home";                                                //視圖名爲home
    }
}

寫完測試了下,好使,

你可能注意到第一件事就是HomeController帶有@Controller註解,很顯然這個註解是用來聲明控制器的,但實際上這個註解對Spirng MVC 自己影響不大。

@Controller是一個構造型(stereotype)的註解。它基於@Component註解。在這裏,它的目的就是輔助實現組件掃描。由於homeController帶有@Controller註解,所以組件掃描器會自動去找到HomeController,並將其聲明爲Spring應用上下文中的bean。

Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    String value() default "";
}

其實你可讓HomeController帶有@Component註解,它所實現的效果是同樣的。可是在表意性上可能差一些,沒法肯定HomeController是什麼組件類型。

HomeController惟一的一個方法,也就是Home方法,帶有@RequestMapping註解,他的Value屬性指定了這個方法所要處理的請求路徑,method屬性細化了它所能處理的HTTP方法,在本例中,當收到對‘/’的HTTP GET請求時,就會調用home方法。

home()方法其實並無作太多的事情,它返回一個String類型的「home」,這個String將會被Spring MVC 解讀爲要渲染的視圖名稱。DispatcherServlet會要求視圖解析器將這個邏輯名稱解析爲實際的視圖。

鑑於咱們配置InternalResourceViewResolver的方式,視圖名「home」將會被解析爲「/WEB-INF/views/home.jsp」

Spittr應用的首頁,定義爲一個簡單的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="<c:url value="/resources/style.css" />" >
  </head>
  <body>
    <h1>Welcome to Spitter</h1>
    <a href="<c:url value="/spittles" />">Spittles</a> |
    <a href="<c:url value="/spitter/register" />">Register</a>
  </body>
</html>

測試控制器最直接的辦法多是構建並部署應用,而後經過瀏覽器對其進行訪問,可是自動化測試可能會給你更快的反饋和更一致的獨立結果,因此,讓咱們編寫一個針對HomeController的測試

5.2.1 測試控制器

編寫一個簡單的類來測試HomoController。

import static org.junit.Assert.*;
import org.junit.Test;

public class HomeControllerTest {
    @Test
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        assertEquals("home",controller.home());
    }
}

在測試中會直接調用home()方法,並斷言返回包含 "home"值的String類型。它徹底沒有站在Spring MVC控制器的視角進行測試。這個測試沒有斷言當接收到針對「/」的GET請求時會調用home()方法。由於它返回的值就是「home」,因此沒有真正判斷home是試圖的名稱。

不過從Spring 3.2開始,咱們能夠按照控制器的方式進行測試Spring MVC中的控制器了。而不只僅是POJO進行測試。Spring如今包含了一種mock Spirng MVC 並針對控制器執行 HTTP請求的機制。這樣的話,在測試控制器的時候,就沒有必要在啓動Web服務器和Web瀏覽器了。

爲了闡述如何測試Spirng MVC 容器,咱們重寫了HomeControllerTest並使用Spring MVC 中新的測試特性。

import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Created by guo on 24/2/2018.
 */
public class HomeControllerTest1  {
    @Test                                                             //你們在測試的時候注意靜態導入的方法
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        MockMvc mockMvc =  standaloneSetup(controller).build();       //搭建MockMvc
       mockMvc.perform(get("/"))                                      //對「/」執行GET請求,
               .andExpect(view().name("home"));                       //預期獲得home視圖
    }
}

此次咱們不是直接調用home方法並測試它的返回值,而是發起了對"/"的請求,並斷言結果視圖的名稱爲home,它首先傳遞一個HomeController實例到MockMvcBuilders.strandaloneSetup()並調用build()來構建MockMvc實例,而後它使用MockMvc實例執行鍼對「/」的GET請求,並設置 指望獲得的視圖名稱。

5.2.2 定義類級別的請求處理。

如今,已經爲HomeController編寫了測試,那麼咱們能夠作一些重構。並經過測試來保證不會對功能形成什麼破壞。咱們能夠作的就是拆分@RequestMapping,並將其路徑映射部分放到類級別上

@Controller
@RequestMapping("/")
public class HomeController {
    @RequestMapping(method = RequestMethod.GET)           //處理對「/」的Get請求
    public String home() {
        return "home";                                    //視圖名爲home
    }
}

在這個新版本的HomeController中,路徑被轉移到類級別的@RequestMapping上,而HTTP方法依然映射在方法級別上。當控制器在類級別上添加@RequestMapping註解時,這個註解會應用到控制器的全部處理器方法上,處理器方法上的@RequestMapping註解會對類級別上的@RequestMapping的聲明進行補充。

就HomeController而言,這裏只有一個控制器方法,與類級別的@RequestMapping合併以後,這個方法的@RequestMapping代表home()將會處理對 「/」路徑的GET請求。

有了測試,因此能夠確保在這個過程當中,沒有對原有的功能形成破壞。

當咱們修改@RequestMapping時,還能夠對HomeController作另外一個變動。@RequestMapping的value接受一個String類型的數組。到目前爲止,咱們給它設置的都是一個String類型的‘/’。可是,咱們還能夠將它映射到對「/Homepage」的請求,只須要將類級別的@RequestMapping改動下

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

如今,HomeController的home()方法能夠被映射到對「/」和「/homepage」的GET請求上。

5.2.3 傳遞模型數據到視圖中

到目前爲止,就編寫超級簡單的控制器來講,HomeController已是一個不錯的樣例了,可是大多數的控制器並非那麼簡單。在Spring應用中,咱們須要有一個頁面展現最近提交的Spittle列表。所以,咱們須要有一個新的方法來處理這個頁面。

首先須要定義一個數據訪問的Repository,爲了實現解耦以及避免陷入數據庫訪問的細節中,咱們將Repository定義爲一個接口,並在稍後實現它(第十章),此時,咱們只須要一個可以獲取Spittle列表的Repository,

package com.guo.spittr.data;
import com.guo.spittr.Spittle;
import java.util.List;
/**
 * Created by guo on 24/2/2018.
 */
public interface SpittleRepository {
    List<Spittle> finfSpittles(long max, int count);
}

findSpittles()方法接受兩個參數,其中max參數表明所返回的Spittle中,Spittle ID屬性的最大值,而count參數代表要返回多少個Spittle對象,爲了得到最新的20個Spittle對象,咱們能夠這樣調用方法。

List<Spittle> recent = SpittleRepository.findSpittles(long.MAX_VALUE(),20)

它的屬性包括消息內容,時間戳,以及Spittle發佈時對應的經緯度。

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

  //Getter和Setter略

  @Override
public boolean equals(Object that) {
  return EqualsBuilder.reflectionEquals(this, that, "id", "time");
}

@Override
public int hashCode() {
  return HashCodeBuilder.reflectionHashCode(this, "id", "time");
}

須要注意的是,咱們使用Apache Common Lang包來實現equals()和hashCode()方法,這些方法除了常規的做用之外,當咱們爲控制器的處理器方法編寫測試時,它們也是有用的。

既然咱們說到了測試,那麼咱們繼續討論這個話題,併爲新的控制器方法編寫測試,

@Test
 public void houldShowRecentSpittles() throws Exception {
   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();

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

測試首先會建立SpittleRepository接口的mock實現,這個實現會從他的findSpittles()方法中返回20個Spittle對象,而後將這個Repository注入到一個新的SpittleController實例中,而後建立MockMvc並使用這個控制器。

須要注意的是這個測試在MockMvc構造器上調用了setSingleView().這樣的話,mock框架就不用解析控制器中的視圖名了。在不少場景中,其實不必這麼作,可是對於這個控制器方法,視圖和請求路徑很是類似,這樣按照默認的駛入解析規則,MockMvc就會發生失敗,由於沒法區分視圖路徑和控制器的路徑,在這個測試中,構建InternalResourceViewResolver時所設置的路徑是可有可無的,但咱們將其設置爲InternalResourceViewResolver一致。

這個測試對「/spittles」發起Get請求,而後斷言視圖的名稱爲spittles而且模型中包含名爲spittleList的屬性,在spittleList中包含預期的內容。

固然若是此時運行測試的話,它將會失敗。他不是運行失敗,而是編譯的時候就失敗,這是由於咱們還沒編寫SpittleController。

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

    @Autowired
    public  SpittleController(SpittleRepository spittleRepository) {              //注入SpittleRepository
        this.spittleRepository = spittleRepository;
    }
    @RequestMapping(method = RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE,20));     // 將spittle添加到視圖
        return "spittles";                                                          // 返回視圖名
    }
}

咱們能夠看到SpittleController有一個構造器,這個構造器使用@Autowired註解,用來注入SpittleRepository。這個SpittleRepository隨後又在spittls()方法中,用來獲取最新的spittle列表。

須要注意的是咱們在spittles()方法中給定了一個Model做爲參數。這樣,spittles()方法就能夠將Repository中獲取到的Spittle列表填充到模型中,Model實際上就是一個Map(也就是key-value的集合)它會傳遞給視圖,這樣數據就能渲染到客戶端了。當調用addAttribute()方法而且指定key的時候,那麼key會根據值的對象類型來推斷肯定。

sittles()方法最後一件事是返回spittles做爲視圖的名字,這個視圖會渲染模型。

若是你但願顯示模型的key的話,也能夠指定,

@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
    model.addAttribute("spittleList",
        spittleRepository.findSpittles(Long.MAX_VALUE,20));     // 將spittle添加到視圖
    return "spittles";                                          // 返回視圖名
}

若是你但願使用非Spring類型的話,那麼可使用java.util.Map來代替Model

@RequestMapping(method = RequestMethod.GET)
public String spittles(Map model) {
    model.put("spittleList",
        spittleRepository.findSpittles(Long.MAX_VALUE,20));     // 將spittle添加到視圖
    return "spittles";                                          // 返回視圖名
}

既然咱們如今提到了各類可替代方案,那下面還有另一種方式來編寫spittles()方法

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

這個並無返回值,也沒有顯示的設定模型,這個方法返回的是Spittle列表。。當處理器方法像這樣返回對象或集合時,這個值會放到模型中,模型的key會根據其類型推斷得出。在本示例中也就是(spittleList)

邏輯視圖的名稱也會根據請求的路徑推斷得出。由於這個方法處理針對「/spittles」的GET請求,所以視圖的名稱將會是spittles,(去掉開頭的線。)

無論使用哪一種方式來編寫spittles()方法,所達成的結果都是相同的。模型會存儲一個Spittle列表,ket爲spittleList,而後這個列表會發送到名爲spittles的視圖中。視圖的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>

儘管SpittleController很簡單,可是它依然比homeController更進一步,不過,SpittleController和HomeController都沒有處理任何形式的輸入。如今,讓咱們擴展SpittleContorller,讓它從客戶端接受一些輸入。

5.3 接受請求的輸入

Spring MVC 容許以多種方法將客戶端中的數據傳送到控制器的處理器方法中

  • 查詢數據(Query Parameter)
  • 表單參數(Form Parameter)
  • 路徑變量(Path Variable)

做爲開始,先來看下如何處理帶有查詢參數的請求,這也是客戶端往服務器發送數據時,最簡單和最直接的方法。

5.3.1 處理查詢參數

在Spittr應用中,可能須要處理的一件事就是展示分頁的Spittle列表,若是你想讓用戶每次查看某一頁的Spittle歷史,那麼就須要提供一種方式讓用戶傳遞參數進來,進而肯定展示那些Spittle列表。

爲了實現這個分頁功能,咱們編寫的處理方法要接受兩個參數

  • before參數 (代表結果中全部的SPittle的ID均在這個值以前)
  • count參數(彪悍在結果中要包含的Spittle數量)

爲了實現這個功能,咱們將程序修改成spittles()方法替換爲使用before參數和count參數的新spittles()方法。

首先添加一個測試,這個測試反映了xinspittles()方法的功能

@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())));
}

這個測試方法關鍵點在於同時傳入了max和count參數,它測試了這些參數存在時的處理方法,而另外一個則測試了沒有這些參數的情景。

在這個測試以後,咱們就能確保無論控制器發生了什麼樣的變化,它都可以處理這兩種類型的請求。

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

SittleController中的處理器方法同時要處理有參數和沒參數的場景,那咱們須要對其進行修改,讓它能接受參數。同時若是這些參數在請求中不存在的話,就是用默認值Long.MAX_VALUE和20.@RequestParam註解的defaultValue屬性能夠完成這個任務。

@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的最大值。

由於查詢參數都是String 類型 ,所以defaultValue屬性須要String類型,

private static final String MAX_LONG_AS_STRING = long.toString(Long.MAX.VALUE)

請求中的查詢參數是往控制器中傳遞信息的經常使用手段。另一種方式就是將傳遞的參數做爲請求路徑的一部分。

5.3.2 經過路徑參數接受輸入

假設咱們的應用程序須要根據給定的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";
}

在理想狀況下,要識別資源應用應該經過URL路徑來標識,而不是經過查詢參數。對「/spittles/12345」發起請求要優於對「/spittles/show?spittle_id=12345」發起的請求。前者能識別出要查詢的資源,然後者描述的是帶有參數的一個操做——本質上是經過HTTP發起的RPC。

既然已經以面向資源的控制器做爲目標,那咱們將這個需求轉化爲一個測試。

@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"))                                 //斷言圖片的名稱爲spittle
    .andExpect(model().attributeExists("spittle"))                     //預期Spittle放到了模型之中
    .andExpect(model().attribute("spittle", expectedSpittle));
}

這個測試構建了一個mockRepository,一個控制器和MockMvc

到目前爲止,咱們所編寫的控制器,全部的方法都映射到了靜態定義好的路徑上,還須要包含變量部分

爲了實現這種路徑變量,Spring MVC容許咱們在@RequestMapping路徑中添加佔位符,佔位符的名稱須要({..}),路徑中的其餘部分要與所處理的請求徹底匹配,可是佔位符但是是任意的值。

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

@PathVariable("spittleId") 代表在請求路徑中,無論佔位符部分的值是什麼都會傳遞給處理器方法的showSpittle參數中。

也能夠去掉這個value的值,由於方法的參數碰巧與佔位符的名稱相同。

@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應用中,咱們須要有個表單讓用戶進行註冊,SitterController是一個新的控制器,目前只有一個請求處理的方法來展示註冊表單。

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

測試展示表單的控制器方法(老外每次都測試)

@Test
public void shouldShowRegistration() throws Exception {
  SpitterController controller = new SpitterController();
  MockMvc mockMvc = standaloneSetup(controller).build();
  mockMvc.perform(get("/spitter/register"))
         .andExpect(view().name("registerForm"));
}
}

這個JSP必須包含一個HTML<form>標籤,

<form method="POST" name="spittleForm">
  <input type="hidden" name="latitude">
  <input type="hidden" name="longitude">
  <textarea name="message" cols="80" rows="5"></textarea><br/>
  <input type="submit" value="Add" />
</form>

須要注意的是這裏的<form>標籤中並無設置action屬性。在這種狀況下,當表單體提交的時,它會提交到與展示時相同的URL路徑上,它會提交到「/spitter/reqister」上。

這意味着須要在服務器端編寫該HTTP POST請求。

5.4.1 編寫處理表單的處理器

當處理註冊表單的POST請求時,控制器須要接受表單數據,並將表單數據保存爲Spitter對象。最後爲了防止重複提交(用戶刷新頁面),應該將瀏覽器重定向到新建立用戶的基本信息頁面。

@Test
public void shouldProcessRegistration() throws Exception {
  SpitterRepository mockRepository = mock(SpitterRepository.class);
  Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov");
  Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov");
  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")
         .param("email", "jbauer@ctu.gov"))
         .andExpect(redirectedUrl("/spitter/jbauer"));

  verify(mockRepository, atLeastOnce()).save(unsaved);
}

但願你們也能夠學會這樣方式

在構建完SpitterRepository的mock實現以及所要執行的控制器和MockNvc以後,shouldProcessRegistration()對「/spitter/register」發起了一個POST請求,做爲請求的一部分,用戶信息以參數的形式放到request中,從而模擬提交的表單。

/**
 * Created by guo on 24/2/2018.
 */
@Controller
@RequestMapping("/spitter")
public class SpitterController {

    private SpitterRepository spitterRepository;

    @Autowired
    public SpitterController(SpitterRepository spitterRepository) {      //注入SpiterRepository
        this.spitterRepository = spitterRepository;
    }

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

    @RequestMapping(value = "/register",method = RequestMethod.POST)
    public String procesRegistration(Spitter spitter) {
        spitterRepository.save(spitter);                                //保存Spitter
        return "redirect:/spitter/" + spitter.getUsername();            //重定向到基本信息頁面
    }
}

返回一個String類型,用來指定視圖。可是這個視圖格式和之前有所不一樣。這裏不只返回了視圖的名稱供視圖解析器查找目標視圖,並且返回的值還帶有重定向的格式return "redirect:/spitter/" 當看到視圖格式中有「redirect:」前綴時,它就知道要將其解析爲重定向的規則,而不是試圖的名稱。在本例中,它將會重定向到基本信息的頁面。

須要注意的是除了能夠「redirect」還能夠識別「forward:」前綴,請求將會前(forward)往指定的URL路徑,而再也不是重定向。

在SpitterController中添加一個處理器方法,用來處理對基本信息頁面的請求。

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

spitterRepository經過用戶獲取一個Spitter對象,showSpitterProfile()方法獲得這個對象並將其添加到模型中,而後返回profile。也就是基本信息頁面的邏輯視圖。

<body>
  <h1>Your Profile</h1>
  <c:out value="${spitter.username}" /><br/>
  <c:out value="${spitter.firstName}" /> <c:out value="${spitter.lastName}" /><br/>
  <c:out value="${spitter.email}" />
</body>


注意:這裏使用H2數據庫,太有用了。

@Configuration
public class DataConfig {

  @Bean
  public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("schema.sql")
            .build();
  }
  @Bean
  public JdbcOperations jdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
  }
}

若是表單中沒有發送username或password,會發生什麼狀況呢?或者名字太長,由會怎麼樣?,接下來,讓咱們看一下爲表單添加校驗,而從避免數據呈現不一致性。

5.4.2 校驗表單

若是用戶在提交表單的時候,username和password爲空的話,那麼將會致使在新建Spitter對象中,username和password是空的String。若是不處理,將會出項安全問題。

同時咱們應該阻止用戶提交空的名字。限制這些輸入的長度。

從Spring 3.0 開始,在Spring MVC中提供了java校驗的API的支持。只須要在類路徑下包含這個JavaAPI的實現便可。好比Hibernate validator.

Java校驗API定義了多個註解,這些註解能夠用在屬性上,從而限制這些屬性的值。

  • @Size :所註解的元素必須是String、集合、或數組,而且長度要符合要求
  • @Null :所註解的值必須爲Null
  • @NotNull :所註解的元素不能爲Null。
  • @Max :所註解的必須是數字,而且值要小於等於給定製。
  • @Min
  • @Past :所註解的元素必須是一個已過時的日期
  • @Future :必須是一個未來的日期
  • @Pattern:必須匹配給定的正則表達式
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
  @Email
  private String email;

  忽略其餘方法。
}
@RequestMapping(value="/register", method=POST)      //老外喜歡靜態導入特性
public String processRegistration(
    @Valid Spitter spitter,                          //校驗Spitter輸入
    Errors errors) {
  if (errors.hasErrors()) {
    return "registerForm";                           //若是校驗出現錯誤,則從新返回表單
  }

  spitterRepository.save(spitter);
  return "redirect:/spitter/" + spitter.getUsername();
}

Spitter參數添加了@Valid註解,這會告訴Spring,須要確保這個對象知足校驗限制。

若是表單出錯的話,那麼這些錯誤能夠經過Errors進行反問。

很重要一點須要注意的是:Errors參數要緊跟在帶有Valid註解參數的後面。@Valid註解所標註的就是要校驗的參數。

若是沒有錯誤的話,Spitter對象將會經過Repository進行保存,控制器會像以前那樣重定向到基本信息頁面。

5.5 小節

在本章中,咱們爲編寫應用程序的Web部分開來一個好頭,能夠看到Spring有一個強大而靈活的Web框架。藉助於註解,Spring MVC 提供了近似於POJO的開發模式,這使得開發處理請求的控制器變得簡單,同時也易於測試。

當編寫控制器的處理方法時,Spring MVC及其靈活。歸納來說,若是你的處理器方法須要內容的話,只需將對應的對象做爲參數,而他不須要的內容,則沒有必要出如今參數列表中。這樣,就爲請求帶來了無限的可能性,同時還能保持一種簡單的編程模型。

儘管本章中不少內容都是關於控制器的請求處理的,可是渲染響應也一樣重要,咱們經過使用JSP的方式,簡單瞭解瞭如何爲控制器編寫視圖,可是,就Spring MVC視圖來講,它並非本章所看到的簡單JSP。

在接下來的第6章,咱們將會更深刻的學習Spring視圖,包括如何在JSP中使用Spring標籤庫,還會學習如何藉助於Apache Tiles爲視圖添加一致的結構。同時,還會了解Thymeleaf,這是一個頗有意思的JSP替代方法,Spring爲其提供了內置的支持。

真的很是期待下一章,,,,,加油

相關文章
相關標籤/搜索