【Spring實戰】構建Spring Web應用程式

weixin_34007291發表於2017-11-07

本章內容:

  • 對映請求到Spring控制器
  • 透明地繫結表單引數
  • 校驗表單提交

狀態管理工作流以及驗證都是Web 開發需要解決的重要特性。HTTP協議的無狀態性決定了這些問題都不那麼容易解決。

Spring的Web框架就是為了幫助解決這些關注點而設計的。Spring MVC基於模型-檢視-控制器(Model-View-Controller,MVC)模式實現,它能構建像Spring框架那樣靈活和鬆耦合的Web應用程式。


Spring MVC起步

Spring將請求在排程Servlet、處理器對映(handler mapping)、控制器以及檢視解析器(view resolver)之間移動。

跟蹤Spring MVC的請求

每當使用者在Web瀏覽器中點選連結或提交表單的時候,請求就開始工作了。請求會將資訊從一個地方帶到另一個地方,就像是快遞投送員。

從離開瀏覽器開始到獲取響應返回,請求會經歷好多站,在每站都會留下一些資訊同時也會帶上其他資訊。

7882361-ca42d26e82026386.png
請求使用Spring MVC所經歷的所有站點

在請求離開瀏覽器時 ①,會帶有使用者所請求內容的資訊,比如請求的URL,使用者提交的表單資訊。

請求旅程的第一站是Spring的DispatcherServlet。與大多數基於Java的Web框架一樣,Spring MVC所有的請求都會通過一個前端控制器(front controller)Servlet。

前端控制器是常用的Web應用程式模式,在這裡一個單例項的Servlet將請求委託給應用程式的其他元件來執行實際的處理。在Spring MVC中,DispatcherServlet就是前端控制器。

DispatcherServlet的任務是將請求傳送給Spring MVC控制器(controller)。控制器是一個用於處理請求的Spring元件。在典型的應用程式會有多個控制器,DispatcherServlet需要知道應該將請求傳送給哪個控制器。所以DispatcherServlet以會查詢一個或多個處理器對映(handler mapping)②來確定請求的下一站在哪裡。處理器對映會根據請求所攜帶的URL資訊來進行決策。

一旦選擇了合適的控制器,DispatcherServlet會將請求傳送給選中的控制器③ 。到了控制器,請求會卸下其負載(使用者提交的資訊)並等待控制器處理這些資訊。(設計良好的控制器本身只處理很少甚至不處理工作,而是將業務邏輯委託給一個或多個服務物件進行處理。)

控制器在完成邏輯處理後,通常會產生一些資訊,這些資訊需要返回給使用者並在瀏覽器上顯示。這些資訊被稱為模型(model)。這些資訊會以使用者友好的方式進行格式化,一般會是HTML。所以,資訊需要傳送給一個檢視(view),通常會是JSP。

控制器所做的最後一件事就是將模型資料打包,並且標示出用於渲染輸出的檢視名。接下來會將請求連同模型和檢視名傳送回DispatcherServlet ④。

這樣控制器就不會與特定的檢視相耦合,傳遞給DispatcherServlet的檢視名並不直接表示某個特定的JSP。甚至並不能確定檢視就是JSP。它僅僅傳遞了一個邏輯名稱,這個名字將會用來查詢產生結果的真正檢視。DispatcherServlet將會使用檢視解析器(view resolver)⑤來將邏輯檢視名匹配為一個特定的檢視實現。

DispatcherServlet知道由哪個檢視渲染結果以後,請求的最後一站是檢視的實現⑥,請求在這裡交付模型資料。請求的任務完成。檢視將使用模型資料渲染輸出,這個輸出會通過響應物件傳遞給客戶端⑦。

請求要經過很多的步驟,最終才能形成返回給客戶端的響應。大多數的步驟都是在Spring框架內部完成的。

若不使用Spring MVC,則需要使用者負責編寫一個Dispatcher servlet,這個Dispatcher servlet要能做如下事情:

  1. 根據URI呼叫相應的action。
  2. 例項化正確的控制器類。
  3. 根據請求引數值來構造表單bean
  4. 呼叫控制器物件的相應方法。
  5. 轉向到一個檢視(JSP頁面)。
搭建Spring MVC

藉助於最近幾個Spring新版本的功能增強,使用Spring MVC變得非常簡單了。現在,使用最簡單的方式來配置Spring MVC:所要實現的功能僅限於執行所建立的控制器。

配置DispatcherServlet

Spring MVC中提供了一個Dispatcher Servlet,它會呼叫控制器方法並轉發到檢視。DispatcherServlet是Spring MVC的核心。在這裡請求會第一次接觸到框架,它要負責將請求路由到其他的元件之中

傳統的方式,像DispatcherServlet這樣的Servlet會配置在web.xml檔案中,這個檔案會放到應用的WAR包裡面。

藉助於Servlet 3規範和Spring 3.1的功能增強,這種方式已經不是唯一的方案了,可以使用Java將DispatcherServlet配置在Servlet容器中:

package spittr.config;

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

import spittr.web.WebConfig;

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  
  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }

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

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

}

擴充套件AbstractAnnotationConfigDispatcherServletInitializer的任意類都會自動地配置Dispatcher-ServletSpring應用上下文,Spring的應用上下文會位於應用程式的Servlet上下文之中。

AbstractAnnotationConfigDispatcherServletInitializer剖析

在Servlet 3.0環境中,容器會在類路徑中查詢實現javax.servlet.ServletContainerInitializer介面的類,如果能發現的話,就會用它來配置Servlet容器。Spring提供了這個介面的實現,名為SpringServletContainerInitializer,這個類反過來又會查詢實現WebApplicationInitializer的類並將配置的任務交給它們來完成。Spring 3.2引入了一個便利的WebApplicationInitializer基礎實現,也就是AbstractAnnotationConfigDispatcherServletInitializer因為我們的Spittr-WebAppInitializer擴充套件了AbstractAnnotationConfigDispatcherServletInitializer(同時也就實現了WebApplicationInitializer),因此當部署到Servlet 3.0容器中的時候,容器會自動發現它,並用它來配置Servlet上下文。

儘管AbstractAnnotationConfigDispatcherServletInitializer的名字很長,但使用起來很簡便。在上面的程式中,SpittrWebAppInitializer重寫了三個方法。

第一個方法是getServletMappings(),它會將一個或多個路徑對映到DispatcherServlet上。在本例中,它對映的是“/”,這表示它會是應用的預設Servlet。它會處理進入應用的所有請求。

要理解其他的兩個方法,首先要理解DispatcherServlet和一個Servlet監聽器(ContextLoaderListener)的關係。

兩個應用上下文之間的故事

DispatcherServlet啟動的時候,它會建立Spring應用上下文,並載入配置檔案或配置類中所宣告的bean。getServletConfigClasses()方法中,指明DispatcherServlet載入應用上下文時,使用定義在WebConfig配置類(使用Java配置)中的bean。

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

我們希望DispatcherServlet載入包含Web元件的bean,如控制器、檢視解析器以及處理器對映,而ContextLoaderListener要載入應用中的其他bean。這些bean通常是驅動應用後端的中間層和資料層元件。

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

如果按照這種方式配置DispatcherServlet,而不是使用web.xml的話,只能部署到支援Servlet 3.0的伺服器中才能正常工作,如Tomcat 7或更高版本。

如果沒有使用支援Servlet 3.0的伺服器,就無法在AbstractAnnotationConfigDispatcherServletInitializer子類中配置DispatcherServlet,只能使用web.xml配置。

啟用Spring MVC

啟用Spring MVC元件的方法不僅一種。使用XML進行配置的,可以使用<mvc:annotation-driven>啟用註解驅動的Spring MVC。

基於Java的配置,使用帶有@EnableWebMvc註解的類:

package spittr.web;

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

@Configuration
@EnableWebMvc
public class WebConfig {
}

但還有其他問題要解決:

  • 沒有配置檢視解析器。Spring預設會使用BeanNameView-Resolver,這個檢視解析器會查詢ID與檢視名稱匹配的bean,並且查詢的bean要實現View介面,以這樣的方式來解析檢視。
  • 沒有啟用元件掃描。Spring只能找到顯式宣告在配置類中的控制器。
  • DispatcherServlet會對映為應用的預設Servlet,所以它會處理所有的請求,包括對靜態資源的請求,如圖片和樣式表(可能並不是想要的效果)。

需要為這個最小的Spring MVC配置再加上一些內容,從而讓它變得真正有用。

package spittr.web;

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.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
  
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }
  
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // TODO Auto-generated method stub
    super.addResourceHandlers(registry);
  }

}

WebConfig現在新增了@Component-Scan註解,因此將會掃描spitter.web包來查詢元件。控制器如果帶有@Controller註解,會使其成為元件掃描時的候選bean。不需要在配置類中顯式宣告任何的控制器。

接著,新增了一個ViewResolver bean。具體來講,是InternalResourceViewResolver。它會查詢JSP檔案,在查詢的時候,會在檢視名稱上加一個特定的字首和字尾(例如,名為home的檢視將會解析為/WEB-INF/views/home.jsp)。

新的WebConfig類還擴充套件了WebMvcConfigurerAdapter並重寫了其configureDefaultServletHandling()方法。通過呼叫DefaultServletHandlerConfigurerenable()方法,要求DispatcherServlet將對靜態資源的請求轉發到Servlet容器中預設的Servlet上,而不是使用DispatcherServlet本身來處理此類請求。

Web相關的配置通過DispatcherServlet建立的應用上下文都已經配置好了,因此現在的RootConfig相對很簡單:

package spittr.config;

import java.util.regex.Pattern;

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={"spitter"}, 
    excludeFilters={
        @Filter(type=FilterType.ANNOTATION, value=EnableWebMVC.class)
    })
public class RootConfig {
}

RootConfig使用了@ComponentScan註解。這樣的話,就有很多機會用非Web的元件來充實完善RootConfig。

現在,已經可以開始使用Spring MVC構建Web應用了。

Spittr應用簡介

因為從Twitter借鑑了靈感並且通過Spring來進行實現,所以它就有了一個名字:Spitter。再進一步,應用網站命名中流行的模式,如Flickr,我們去掉字母e,這樣的話,我們就將這個應用稱為Spittr。這個名稱也 有助於區分應用名稱和領域型別,因為我們將會建立一個名為Spitter的領域類。

Spittr應用有兩個基本的領域概念:Spitter(應用的使用者)和Spittle(使用者釋出的簡短狀態更新)。當我們在書中完善Spittr應用的功能時,將會介紹這兩個領域概念。在本章中,會構建應用的Web層,建立展現Spittle的控制器以及處理使用者註冊成為Spitter的表。

編寫基本的控制器

在Spring MVC中,控制器只是方法上新增了@RequestMapping註解的類,這個註解宣告瞭它們所要處理的請求。

假設控制器類要處理對“/”的請求,並渲染應用的首頁:

package spittr.web;

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

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = GET)
    public String home(){
        return "home";
    }
}

HomeController是一個構造型(stereotype)的註解,它基於@Component註解,輔助實現元件掃描。因為HomeController帶有@Controller註解,因此元件掃描器會自動找到HomeController,並將其宣告為Spring應用上下文中的一個bean。

可以讓HomeController帶有@Component註解,效果相同,但是表意性會差一點。

HomeController唯一的一個方法,也就是home()方法,帶有@RequestMapping註解。它的value屬性指定了這個方法所要處理的請求路徑,method屬性細化了它所處理的HTTP方法。當收到對“/”的HTTP GET請求時,就會呼叫home()方法。

home()方法返回了一個String型別的“home”。這個String將會被Spring MVC解讀為要渲染的檢視名稱。DispatcherServlet會要求檢視解析器將這個邏輯名稱解析為實際的檢視。

根據檢視解析器InternalResourceViewResolver的配置,檢視名“home”將會解析為“/WEB-INF/views/home.jsp”路徑的JSP。

定義一個簡單的首頁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>

這個JSP提供了兩個連結:一個是檢視Spittle列表,另一個是在應用中進行註冊。下圖展現了此時的首頁的樣子:

7882361-7f109f0f5010e265.png
執行效果

現在,對這個控制器發起一些請求,看一下它是否能夠正常工作。測試控制器最直接的辦法可能就是構建並部署應用,然後通過瀏覽器對其進行訪問,但是自動化測試可提供更快的反饋和更一致的獨立結果

測試控制器

編寫一個簡單的類來測試HomeController:

package java.spittr.web;

import static org.junit.Assert.assertEquals;
import org.junit.Test;
import spittr.web.HomeController;

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

}

程式碼只測試了home()方法中會發生什麼。在測試中會直接呼叫home()方法,並斷言返回包含“home”值的String。它完全沒有站在Spring MVC控制器的視角進行測試。這個測試沒有斷言當接收到針對“/”的GET請求時會呼叫home()方法。因為它返回的值就是“home”,所以也沒有真正判斷home是檢視的名稱。

從Spring 3.2開始,我們可以按照控制器的方式來測試Spring MVC中的控制器了,而不僅僅是作為POJO進行測試。Spring現在包含了一種mock Spring MVC並針對控制器執行HTTP請求的機制。這樣的話,在測試控制器的時候,就沒有必要再啟動Web伺服器和Web瀏覽器了。

重寫HomeControllerTest並使用Spring MVC中新的測試特性:

package spittr.web;

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

public class HomeControllerTest {
    @Test
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(get("/")).andExpect(view().name("home"));
    }

新版本的測試比之前更加完整地測試了HomeController。這次我們不是直接呼叫home()方法並測試它的返回值,而是發起了對“/”的GET請求,並斷言結果檢視的名稱為home。它首先傳遞一個HomeController例項到MockMvcBuilders.standaloneSetup()並呼叫build()來構建MockMvc例項。然後它使用MockMvc例項來執行鍼對“/”的GET請求並設定期望得到的檢視名稱。

定義類級別的請求處理

拆分@RequestMapping,並將其路徑對映部分放到類級別上:

package spittr.web;

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

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @RequestMapping(method = GET)
    public String home(){
        return "home";
    }
}

新版本的HomeController中,路徑現在被轉移到類級別的@RequestMapping上,而HTTP方法依然對映在方法級別上。當控制器在類級別上新增@RequestMapping註解時,這個註解會應用到控制器的所有處理器方法上。處理器方法上的@RequestMapping註解會對類級別上的@RequestMapping的宣告進行補充。

HomeController只有一個控制器方法。與類級別的@Request-Mapping合併之後,這個方法的@RequestMapping表明home()將會處理對“/”路徑的GET請求。

測試程式碼可以確保在這個修改過程中,沒有對原有的功能造成破壞。

當我們在修改@RequestMapping時,還可以對HomeController做另外一個變更@RequestMapping的value屬效能夠接受一個String型別的陣列。到目前為止,我們給它設定的都是一個String型別的“/”。但是,我們還可以將它對映到對“/homepage”的請求,只需將類級別的@RequestMapping改為如下所示:

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

現在,HomeControllerhome()方法能夠對映到對“/”和“/homepage”的GET請求。

傳遞模型資料到檢視中

大多數的控制器並不像HomeController那麼簡單。在Spittr應用中,我們需要有一個頁面展現最近提交的Spittle列表。因此,我們需要一個新的方法來處理這個頁面。

首先,需要定義一個資料訪問的Repository。為了實現解耦以及避免陷入資料庫訪問的細節之中,我們將Repository定義為一個介面,並在稍後實現它(第10章中)。此時,我們只需要一個能夠獲取Spittle列表的Repository,如下所示的SpittleRepository功能已經足夠了:

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

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

findSpittles()方法接受兩個引數。其中max引數代表所返回的Spittle中,Spittle ID屬性的最大值,而count參數列明要返回多少個Spittle物件。為了獲得最新的20個Spittle物件,我們可以這樣呼叫findSpittles():

List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE,20);

現在,定義一個Spittle類,讓Spittle類儘可能的簡單。它的屬性包括訊息內容、時間戳以及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(message,time,null,null);
    }

    public Spittle(String message, Date time, Double latitude, Double longitude){
        this.id = null;
        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 getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

    @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來斷言新的處理器方法中所期望的行為:

    @Test
    public void shouldShowRecentSpittles() 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就會發生失敗,因為無法區分檢視路徑和控制器的路徑。在這個測試中,構建InternalResourceView時所設定的實際路徑是無關緊要的,但我們將其設定為與InternalResourceViewResolver配置一致。

這個測試對“/spittles”發起GET請求,然後斷言檢視的名稱為spittles並且模型中包含名為spittleList的屬性,在spittleList中包含預期的內容。

如果此時執行測試的話,它將會失敗。它不是執行失敗,而是在編譯的時候就會失敗。這是因為我們還沒有編寫SpittleController。現在建立一個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 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的話,也可以這樣進行指定:

    @RequestMapping(method = RequestMethod.GET)
    public String spittles(Model model) {
        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",
                spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        return "spittles";
    }

還有一種編寫spittles()的方式:

    @RequestMapping(method = RequestMethod.GET)
    public List<Spittles> spittles() {
        return spittleRepository.findSpittles(Long.MAX_COUNT, 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>

執行結果如圖,由於沒有初始化資料,因此結果列表為空:

7882361-9af5ad64b5aa5818.png

接受請求的輸入

儘管SpittleController很簡單,但是它依然比HomeController更進一步了。但沒有處理任何形式的輸入。現在,我們要擴充套件SpittleController,讓它從客戶端接受一些輸入。

Spring MVC允許以多種方式將客戶端中的資料傳送到控制器的處理器方法中,包括:

  • 查詢引數(Query Parameter)。
  • 表單引數(Form Parameter)。
  • 路徑變數(Path Variable)。
處理查詢引數

帶有查詢引數的請求,這也是客戶端往伺服器端傳送資料時,最簡單和最直接的方式。

在Spittr應用中,我們可能需要處理的一件事就是展現分頁的Spittle列表。在現在的SpittleController中,它只能展現最新的Spittle,沒有辦法向前翻頁檢視以前編寫的Spittle歷史記錄。如果想讓使用者每次都能檢視某一頁的Spittle歷史,那麼就需要提供一種方式讓使用者傳遞引數進來,進而確定要展現哪些Spittle集合。

假設要檢視某一頁Spittle列表,這個列表會按照最新的Spittle在前的方式進行排序。因此,下一頁中第一條的ID肯定會早於當前頁最後一條的ID。所以,為了顯示下一頁的Spittle,我們需要將一個Spittle的ID傳入進來,這個ID要恰好小於當前頁最後一條Spittle的ID。另外,還可以傳入一個引數來確定要展現的Spittle數量。

為了實現這個分頁的功能,編寫的處理器方法要接受如下的引數:

  • before引數(表明結果中所有Spittle的ID均應該在這個值之前)。
  • count引數(表明在結果中要包含的Spittle數量)。

首先新增一個測試,這個測試反映了新spittles()方法的功能。

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

測試方法關鍵針對“/spittles”傳送GET請求,同時還傳入了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);
    }

SpittleController中的處理器方法要同時處理有引數和沒有引數的場景,需要對其進行修改,讓它能接受引數,同時,如果這些引數在請求中不存在的話,就使用預設值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型別的值。因此,使用Long.MAX_VALUE是不行的。我們可以將Long.MAX_VALUE轉換為名為MAX_LONG_-AS_STRING的String型別常量:

private static final String MAX_LONG_AS_STRING = 
        Long.toString(Long.MAX_VALUE);

儘管defaultValue屬性給定的是String型別的值,但是當繫結到
方法的max引數時,它會轉換為Long型別。

如果請求中沒有count引數,count引數會被設為預設值20。

通過路徑引數接受輸入

在構建面向資源的控制器時,這種方式就是將傳遞引數作為請求路徑的一部分,實現資訊的輸入。

假設應用程式需要根據給定的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”這樣的請求。儘管這也可以正常工作,但是從面向資源的角度來看這並不理想。理想情況下,要識別的資源(Spittle)應該通過URL路徑進行標示,而不是通過查詢引數。對“/spittles/12345”發起GET請求要優於對“/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"))
      .andExpect(model().attributeExists("spittle"))
      .andExpect(model().attribute("spittle", expectedSpittle));
  }

這個測試構建了一個mock Repository、一個控制器和MockMvc,這與本章中我們所編寫的其他測試很類似。這個測試的最後幾行,它對“/spittles/12345”發起GET請求,然後斷言檢視的名稱是spittle,並且預期的Spittle物件放到了模型之中。由於我們還沒有為這種請求實現處理器方法,因此這個請求將會失敗。但是,可以通過為SpittleController新增新的方法來修正這個失敗的測試。

到目前為止,在我們編寫的控制器中,所有的方法都對映到了(通過@RequestMapping)靜態定義好的路徑上。但是,如果想讓這個測試通過的話,我們編寫的@RequestMapping要包含變數部分,這部分代表了Spittle ID。

為了實現這種路徑變數,Spring MVC允許我們在@RequestMapping路徑中新增佔位符。佔位符的名稱要用大括號(“{”和“}”)括起來。路徑中的其他部分要與所處理的請求完全匹配,但是佔位符部分可以是任意的值。

下面的處理器方法使用了佔位符,將Spittle ID作為路徑的一部分:

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

spittle()方法的spittleId引數上新增了@PathVariable("spittleId")註解,這表明在請求路徑中,不管佔位符部分的值是什麼都會傳遞到處理器方法的spittleId引數中。如果對“/spittles/54321”傳送GET請求,那麼將會把“54321”傳遞進來,作為spittleId的值。

因為方法的引數名碰巧與佔位符的名稱相同,因此可以去掉@PathVariable中的value屬性:

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

如果@PathVariable中沒有value屬性,它會假設佔位符的名稱與方法的引數名相同。這能夠讓程式碼稍微簡潔一些。但如果你想要重新命名引數時,必須要同時修改佔位符的名稱,使其互相匹配。

spittle()方法會將引數傳遞到SpittleRepository的findOne()方法中,用來獲取某個Spittle物件,然後將Spittle物件新增到模型中。模型的key將會是spittle,這是根據傳遞到addAttribute()方法中的型別推斷得到的。

這樣Spittle物件中的資料就可以渲染到檢視中了,此時需要引用請求中key為spittle的屬性(與模型的key一致)。渲染Spittle的JSP檢視片段如下:

    <div class="spittleView">
      <div class="spittleMessage"><c:out value="${spittle.message}" /></div>
      <div>
        <span class="spittleTime"><c:out value="${spittle.time}" /></span>
      </div>
    </div>

如果傳遞請求中少量的資料,查詢引數和路徑變數是很合適的。但通常還需要傳遞很多的資料(也許是表單提交的資料),那查詢引數顯得有些笨拙和受限了。

處理表單

Web應用的功能通常並不侷限於為使用者推送內容。大多數的應用允許使用者填充表單並將資料提交回應用中,通過這種方式實現與使用者的互動。像提供內容一樣,Spring MVC的控制器也為表單處理提供了良好的支援。

使用表單分為兩個方面:展現表單以及處理使用者通過表單提交的資料。在Spittr應用中,我們需要有個表單讓新使用者進行註冊。SpitterController是一個新的控制器,目前只有一個請求處理的方法來展現登錄檔單。

package spittr.web;

import static org.springframework.web.bind.annotation.RequestMethod.*;

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

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

showRegistrationForm()方法的@RequestMapping註解以及類級別上的@RequestMapping註解組合起來,宣告瞭這個方法要處理的是針對“/spitter/register”的GET請求。按照配置InternalResourceViewResolver,這意味著會使用“/WEB-INF/ views/registerForm.jsp”這個JSP來渲染登錄檔單。

儘管showRegistrationForm()方法非常簡單,但測試依然需要覆蓋到它:

package spittr.web;

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

import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;

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

對“/spitter/register”傳送GET請求,然後斷言結果的檢視名為registerForm。

因為檢視的名稱為registerForm,所以JSP的名稱需要為registerForm.jsp。這個JSP必須要包含一個HTML<form>標籤,在這個標籤中使用者輸入註冊應用的資訊:

<%@ 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>Register</h1>

        <form method="POST">
            First Name: <input type="text" name="firstName" /><br/>
            Last Name: <input type="text" name="lastName" /><br/>
            Email: <input type="email" name="email" /><br/>
            Username: <input type="text" name="username" /><br/>
            Password: <input type="password" name="password" /><br/>
            <input type="submit" value="Register" />
        </form>
    </body>
</html>

表單域中記錄使用者的名字、姓氏、使用者名稱以及密碼,還包含一個提交表單的按鈕。在瀏覽器渲染之後,它的樣子如下所示。

7882361-218b00ea7f596974.png

這裡的<form>標籤中並沒有設定action屬性。在這種情況下,當表單提交時,會提交到與展現時相同的URL路徑上。即提交到“/spitter/register”上。

意味著要在伺服器端處理該HTTP POST請求。需要在Spitter-Controller中再新增一個方法來處理這個表單提交。

編寫處理表單的控制器

當處理登錄檔單的POST請求時,控制器需要接受表單資料並將表單資料儲存為Spitter物件。為了防止重複提交,應該將瀏覽器重定向到新建立使用者的基本資訊頁面。這些行為通過下面的shouldProcessRegistration()進行了測試:

    @Test
    public void shouProcessRegistration() 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實現以及所要執行的控制器和MockMvc之後,shouldProcess-Registration()對“/spitter/register”發起了一個POST請求。作為請求的一部分,使用者資訊以引數的形式放到request中,從而模擬提交的表單。

在處理POST型別的請求時,在請求處理完成後,最好進行一下重定向,這樣瀏覽器的重新整理就不會重複提交表單了。在這個測試中,預期請求會重定到“/spitter/jbauer”,也就是新建使用者的基本資訊頁面。

最後,測試會校驗SpitterRepository的mock實現最終會真正用來儲存表單上傳入的資料。

通過shouldProcessRegistration()方法實現處理表單提交的控制器方法:

package spittr.web;

import static org.springframework.web.bind.annotation.RequestMethod.*;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import spittr.Spitter;
import spittr.data.SpitterRepository;

@Controller
@RequestMapping("/spitter")
public class SpitterController {

  private SpitterRepository spitterRepository;

  @Autowired
  public SpitterController(SpitterRepository spitterRepository) {
    this.spitterRepository = spitterRepository;
  }
  
  @RequestMapping(value="/register", method=GET)
  public String showRegistrationForm() {
    return "registerForm";
  }
  
  @RequestMapping(value="/register", method=POST)
  public String processRegistration(
      @Valid Spitter spitter, 
      Errors errors) {
    if (errors.hasErrors()) {
      return "registerForm";
    }
    
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
  }
  
  @RequestMapping(value="/{username}", method=GET)
  public String showSpitterProfile(@PathVariable String username, Model model) {
    Spitter spitter = spitterRepository.findByUsername(username);
    model.addAttribute(spitter);
    return "profile";
  }
  
}

新建立的processRegistration()方法,它接受一個Spitter物件作為引數。這個物件有firstName、lastName、username和password屬性,這些屬性將會使用請求中同名的引數進行填充。

當使用Spitter物件呼叫processRegistration()方法時,它會呼叫SpitterRepository的save()方法,SpitterRepository是在SpitterController的構造器中注入進來的。

processRegistration()方法最後返回一個String型別,用來指定檢視。但是這個檢視格式和以前我們所看到的檢視有所不同。不僅返回了檢視的名稱供檢視解析器查詢目標檢視,返回的值還帶有重定向的格式。

當InternalResourceViewResolver看到檢視格式中的“redirect:”字首時,它就知道要將其解析為重定向的規則,而不是檢視的名稱。

本例中,它將會重定向到使用者基本資訊的頁面。

除了“redirect:”,InternalResourceViewResolver還能識別“forward:”字首。當它發現檢視格式中以“forward:”作為字首時,請求將會前往(forward)指定的URL路徑,而不是重定向。

儘管HttpServletResponse.sendRedirect方法和RequestDispatcher.forward方法都可以讓瀏覽器獲 得另外一個URL所指向的資源,但兩者的內部執行機制有著很大的區別。
Forward和Redirect代表了兩種請求轉發方式:直接轉發和間接轉發。

  • 直接轉發方式(Forward),客戶端和瀏覽器只發出一次請求,Servlet、HTML、JSP或其它資訊資源,由第二個資訊資源響應該請求,在請求物件request中,儲存的物件對於每個資訊資源是共享的。轉發頁面和轉發到的頁面可以共享request的資料
  • 間接轉發方式(Redirect)實際是兩次HTTP請求,伺服器端在響應第一次請求的時候,讓瀏覽器再向另外一個URL發出請求,從而達到轉發的目的。轉發頁面和轉發到的頁面不能共享request的資料

因為我們重定向到了使用者基本資訊頁面,那麼我們應該往SpitterController中新增一個處理器方法showSpitterProfile(),用來處理對基本資訊頁面的請求:

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

SpitterRepository通過使用者名稱獲取一個Spitter物件,showSpitter-Profile()得到這個物件並將其新增到模型中,然後返回profile,也就是基本資訊頁面的邏輯檢視名。基本資訊檢視實現如下:

<%@ 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>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>
</html>

基本資訊檢視渲染效果如下圖所示:

7882361-cde8a4225715ee6d.png

如果表單中沒有傳送username或password的話,如果firstName或lastName的值為空或太長的話,程式會出現問題,為此需要為表單提交新增校驗,避免資料呈現的不一致性。

校驗表單

如果使用者在提交表單的時候,username或password文字域為空的話,會導致在新建Spitter物件中,username或password是空的String。如果這種現象不處理的話,這將會出現安全問題,不管是誰只要提交一個空的表單就能登入應用。

同時應該阻止使用者提交空的firstName和/或lastName,使應用僅在一定程度上保持匿名性。可以限制這些輸入域值的長度,保持它們的值在一個合理的長度範圍,避免這些輸入域的誤用。

從Spring 3.0開始,在Spring MVC中提供了對Java校驗API的支援。在Spring MVC中使用Java校驗API,不需要什麼額外的配置。只要保證在類路徑下包含這個Java API的實現即可,比如Hibernate Validator。

Java校驗API定義了多個註解,這些註解可以放到屬性上,從而限制這些屬性的值。所有的註解都位於javax.validation.constraints包中。下表列出了這些校驗註解:

7882361-73e407c5ecb90644.png
7882361-3d71efb28aeb97f0.png

除了表中的註解,Java校驗API的實現可能還會提供額外的校驗註解。同時,也可以定義自己的限制條件。

修改Spitter類,為屬性新增校驗註解:

package spittr;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

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;

    . . .
}

現在,Spitter的所有屬性都新增了@NotNull註解,以確保它們的值不為null。屬性上也新增了@Size註解以限制它們的長度在最大值和最小值之間。對Spittr應用來說,這意味著使用者必須要填完登錄檔單,並且值的長度要在給定的範圍內。

接下來要修改processRegistration()方法來應用校驗功能。啟用校驗功能的processRegistration()如下所示:

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

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

Spitter引數新增了@Valid註解,這會告知Spring,需要確保這個物件滿足校驗限制。

在Spitter屬性上新增校驗限制並不能阻止表單提交。即便使用者輸入不符合規範
,processRegistration()方法依然會被呼叫。

如果有校驗出現錯誤的話,那麼這些錯誤可以通過Errors物件進行訪問,現在這個物件作為processRegistration()方法的引數。(需要注意,Errors引數要緊跟在帶有@Valid註解的引數後面。)

processRegistration()方法所做的第一件事就是呼叫Errors.hasErrors()來檢查是否有錯誤。如果有錯誤的話,Errors.hasErrors()將會返回到registerForm,也就是登錄檔單的檢視。這能夠讓使用者的瀏覽器重新回到登錄檔單頁面,所以他們能夠修正錯誤,然後重新嘗試提交。

如果沒有錯誤的話,Spitter物件將會通過Repository進行儲存,重定向到基本資訊頁面。

小結

Spring有一個強大靈活的Web框架。藉助於註解,Spring MVC提供了近似於POJO的開發模式,這使得開發處理請求的控制器變得非常簡單,同時也易於測試。

當編寫控制器的處理器方法時,Spring MVC極其靈活。如果你的處理器方法需要內容的話,只需將對應的物件作為引數,而它不需要的內容,則沒有必要出現在引數列表中。這樣,就為請求處理帶來了無限的可能性,同時還能保持一種簡單的程式設計模型。

相關文章