天天看點

《Spring實戰》學習筆記-第五章:建構Spring web應用

之前一直在看《Spring實戰》第三版,看到第五章時發現很多東西已經過時被廢棄了,于是現在開始讀《Spring實戰》第四版了,章節安排與之前不同了,裡面應用的應該是最新的技術。

本章中,将會接觸到Spring MVC基礎,以及如何編寫控制器來處理web請求,如何通明地綁定請求參數到業務對象上,同時還可以提供資料校驗和錯誤處理的功能。

Spring MVC初探

跟蹤Spring MVC請求

請求會由DispatcherServlet配置設定給控制器(根據處理器映射來确定),在控制器完成處理後,接着請求會被發送給一個視圖來呈現結果

在請求離開浏覽器時,會帶有使用者所請求内容的資訊,例如請求的URL、使用者送出的表單資訊。

請求旅程的第一站是Spring的DispatcherServlet。Spring MVC所有的請求都會通過一個前端控制器Servlet。前端控制器是常用的Web應用程式模式,在這裡一個單執行個體的Servlet将請求委托給應用程式的其他元件來執行實際的處理。在Spring MVC中,DispatcherServlet 就是前端控制器。

DispatcherServlet的任務是将請求發送給Spring MVC控制器。控制器是一個用于處理請求的Spring元件。在典型的應用程式中可能會有多個控制器, Dispatcher Servlet需要知道應該将請求發送給哪個控制器。是以DispatcherServlet會查詢一個或多個處理器映射來确定請求的下一站在哪裡。處理器映射會根據請求所攜帶的URL資訊來進行決策。

一旦選擇了合适的控制器,DispatcherServlet會将請求發送給選中的控制器。到達了控制器,請求會卸下其負載(使用者送出的資訊)并等待控制器處理這些資訊(實際上,設計良好的控制器本身隻處理很少甚至不處理工作,而是将業務邏輯委托給個或多個服務對象)。

控制器在完成邏輯處理後通常會産生一些資訊,這些資訊需要傳回給使用者并在浏覽器上顯示。這些資訊被稱為模型(Model)。不過僅僅給使用者傳回原始的資訊是不夠的--這些資訊需要以使用者友好的方式進行格式化,一般是HTML。是以,資訊需要發送給—個視圖(View),通常會是JSP。

控制器所做的最後一件事是将模型資料打包,并且标示出用于渲染輸出的視圖名稱。它接下來會将請求連同模型和視圖名稱發送回DispatcherServlet。

這樣,控制器就不會與特定的視圖相耦合,傳遞給DispatcherServlet的視圖名稱并不直接表示某個特定的JSP。它僅僅傳遞了一個邏輯名,這個名字将會用來查找用來産生結果的真正視圖。DispatcherServlet将會使用視圖解析器來将邏輯視圖名比對為一個特定的視圖實作。

既然DispatcherServlet已經知道由哪個視圖渲染結果,那麼請求的任務基本上也就完成了。它的最後一站是視圖的實作(可能是JSP),在這裡它傳遞模型資料。請求的任務就完成了。視圖将使用模型資料渲染輸出,并通過這個輸出将響應對象傳遞給用戶端。

搭建Spring MVC

配置DispatcherServlet

DispatcherServlet是Spring MVC的核心,它負責将請求分發到其他各個元件。

在舊版本中,DispatcherServlet之類的servlet一般在

web.xml

檔案中配置,該檔案一般會打包進最後的war包種;但是Spring3引入了注解,我們在這一章将展示如何基于注解配置Spring MVC。

注意:

在使用maven建構web工程時,由于缺少web.xml檔案,可能會出現

web.xml is missing and <failOnMissingWebXml> is set to true

這樣的錯誤,那麼可以通過在pom.xml檔案中添加如下配置來避免這種錯誤:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <failOnMissingWebXml>false</failOnMissingWebXml>
            </configuration>
        </plugin>
    </plugins>
</build>
           

既然不适用

web.xml

檔案,你需要在servlet容器中使用Java配置DispatcherServlet,具體的代碼列舉如下:

package spittr.config;

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

public class SpittrWebAppInitializer 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

的類都會被自動用來配置DispatcherServlet,這個類負責配置DispatcherServlet、初始化Spring MVC容器和Spring容器。

SpittrWebAppInitializer重寫了三個方法,

getRootConfigClasses()

方法用于擷取Spring應用容器的配置檔案,這裡我們給定預先定義的RootConfig.class;

getServletConfigClasses()

負責擷取SpringMVC應用容器,這裡傳入預先定義好的WebConfig.class;

getServletMappings()

方法負責指定需要由DispatcherServlet映射的路徑,這裡給定的是"/",意思是由DispatcherServlet處理所有向該應用發起的請求。

兩種應用上下文

當DispatcherServlet啟動時,會建立一個Spring應用上下文并且會加載配置檔案中聲明的bean,通過

getServletConfigClasses()

方法,DispatcherServlet會加載

WebConfig

配置類中所配置的bean。

在Spring web應用中,通常還有另外一種應用上下文:

ContextLoaderListener

DispatcherServlet用來加載web元件bean,如控制器(controllers)、視圖解析器(view resolvers)以及處理器映射(handler mappings)等。而ContextLoaderListener則用來加載應用中的其他bean,如運作在應用背景的中間層和資料層元件。

AbstractAnnotationConfigDispatcherServletInitializer會同時建立DispatcherServlet和ContextLoaderListener。

getServletConfigClasses()

方法傳回的

@Configuration

類會定義DispatcherServlet應用上下文的bean。同時,

getRootConfigClasses()

傳回的

@Configuration

類用來配置ContextLoaderListener上下文建立的bean。

相對于傳統的

web.xml

檔案配置的方式,通過AbstractAnnotationConfigDispatcherServletInitializer來配置DispatcherServlet是一種替代方案。需要注意的是,這種配置隻适用于Servlet 3.0,例如Apache Tomcat 7或者更高。

激活Spring MVC

正如有多種方式可以配置DispatcherServlet,激活Spring MVC元件也有不止一種方法。一般的,都會通過XML配置檔案的方式來配置Spring,例如可以通過

<mvc:annotation-driven>

來激活基于注解的Spring MVC。

最簡單的配置Spring MVC的一種方式是通過

@EnableWebMvc

注解:

package 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了,但是還有其他一些問題:

  • 沒有配置視圖解析器(view resolvers),這種情況下,Spring會預設使用

    BeanNameViewResolver

    ,它會通過尋找那些與視圖id比對的bean以及實作了View接口的類進行視圖解析;
  • 沒有激活元件掃描:這樣Spring會尋找配置中明确聲明的任意控制器;
  • DispatcherServlet會處理所有的請求,包括靜态資源請求,如圖檔和樣式(這些往往不是我們想要的)。

是以,需要為WebConfig增加一些配置:

package spittr.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("spitter.web") // 激活Spring MVC
public class WebConfig extends WebMvcConfigurerAdapter {

    // 配置一個JSP視圖解析器
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB_INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}
           

首先需要注意的是,

WebConfig

使用了

@ComponentScan

注解,是以會在

spitter.web

包下掃描尋找元件,這些元件包括使用

@Controller

進行注解的控制器。這樣就不再需要在配置類中顯式地聲明其他控制器。

接下來,添加了一個

ViewResolver

bean,即

InternalResourceViewResolver

。它通過比對符合設定的字首和字尾的視圖來用來尋找對應的JSP檔案,比如視圖home會被解析為/WEB-INF/views/home.jsp。這裡的三個函數的含義依次是:

setPrefix()

方法用于設定視圖路徑的字首;

setSuffix()

用于設定視圖路徑的字尾,即如果給定一個邏輯視圖名稱——"home",則會被解析成"/WEB-INF/views/home.jsp";

setExposeContextBeansAsAttributes(true)

使得可以在JSP頁面中通過${}通路容器中的bean。

然後,

WebConfig

繼承自

WebMvcConfigurerAdapter

,并且重寫了

configureDefaultServletHandling()

方法,通過調用

enable()

方法進而可以讓DispatcherServlet将靜态資源的請求轉發給預設的servlet。

package spittr.config;

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

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

}
           

需要注意的一點是,RootConfig 使用了

@ComponentScan

注解。

Spittr應用介紹

這一章要用的例子應用,從Twitter擷取了一些靈感,是以最開始叫Spitter;然後又借鑒了最近比較流行的網站Flickr,是以我們也把e去掉,最終形成Spittr這個名字。這也有利于區分領域名稱(類似于twitter,這裡用spring實作,是以叫spitter)和應用名稱。

Spittr類似于Twitter,使用者可以通過它添加一些推文。Spittr有兩個重要的概念:spitter(應用的使用者)和spittle(使用者釋出簡單狀态)。本章将會建構該應用的web層、建立用于展示spittle的控制器以及使用者注冊流程。

編寫簡單的控制器

Spring MVC中,控制器僅僅是擁有

@RequestMapping

注解方法的類,進而可以聲明它們可以處理何種請求。

在開始之前,我們先假設一個控制器,它可以處理比對

/

的請求并會跳轉到首頁面。

package spittr.web;

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

@Controller // 聲明一個控制器
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET) // 處理GET請求
    public String home() {
        return "home";
    }

}
           

@Controller

是一個構造型注解,它基于

@Component

,元件掃描器會自動地将HomeController聲明為Spring上下文的一個bean。

home()方法采用了

@RequestMapping

注解,屬性

value

指定了該方法處理的請求路徑,

method

方法指定了可以處理的HTTP方法。這種情況下,當一個來自

/

的GET方法請求時,就會調用home()方法。

home()方法僅僅傳回了一個"home"的String值,Spring MVC會對這個String值進行解析并跳轉到指定的視圖上。

DispatcherServlet

則會請求視圖解析器将這個邏輯視圖解析到真實視圖上。

我們已經配置了InternalResourceViewResolver,“home”視圖會被解析到

/WEB-INF/views/home.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

<html>
<head>
<meta charset="utf-8">
<title>Spittr</title>
<link rel="stylesheet" type="text/css"
    href="<c:url value="/resources/style.css" />">
</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>
           

下面對HomeController進行測試。

測試控制器

一般的web測試需要将工程釋出到一個web容器中,啟動後才能觀察運作結果。

如:

首頁

從另外的角度來看,HomeController其實是一個簡單的POJO對象,那麼可以使用下面的方法對其進行測試:

package spittr.web;

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

public class HomeControllerTest {

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

}
           

相對于直接調用home()方法測試它的傳回值,上面的測試中發起了一個來自

/

的 GET 請求,并且對其結果視圖進行斷言。将HomeController的執行個體傳送給

MockMvcBuilders.standaloneSetup

,并且調用

build()

方法來建立一個

MockMvc

執行個體。然後,使用

MockMvc

執行個體産生了一個GET請求,并且設定了視圖的期望。

定義類層級的請求處理

package spittr.web;

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

@Controller // 聲明一個控制器
@RequestMapping("/") // 控制器比對路徑
public class HomeController {

    @RequestMapping(method = RequestMethod.GET) // 處理GET請求
    public String home() {
        return "home";// 視圖名稱
    }

}
           

在這個新版的HomeController中,将請求比對路徑移到了類層級,HTTP方法的比對仍處在方法層級。當有控制類中有一個類層級的

@RequestMapping

,該類中所有的用

@RequestMapping

注解的處理方法共同組成了類層級的

@RequestMapping

@RequestMapping

的value屬性接受String數組,那麼就可以使用如下配置:

@Controller // 聲明一個控制器
@RequestMapping("/", "/homepage") // 控制器比對路徑
public class HomeController {
...
}
           

這種情況下,home()方法就可以處理來自

/

/homepage

的GET請求。

将model資料傳送給視圖

在Spittr應用中,需要一個頁面,用來顯示最近送出的spittle清單。首先需要定義一個資料通路的倉庫,用來抓取spittle:

package spittr.data;

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

public interface SpittleRepository {
    /**
     * @param max
     *            待傳回的最大的Spittle ID
     * @param count
     *            傳回Spittle對象的個數
     * @return
     */
    List<Spittle> findSpittles(long max, int count);
}
           

如果要擷取最近的20個Spittle對象,那麼隻需調用這樣調用:

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

下面對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.latitude = latitude;
        this.longitude = longitude;
    }

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

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

Spittle對象中現在包含資訊、時間戳、位置這幾個屬性。

下面利用Spring的

MockMvc

來斷言新的控制器的行為是否正确:

上面的測試通過建立一個SpittleRepository接口的mock實作,該實作會通過findSpittles()方法傳回一個包含20個Spittle對象的集合。然後将這個bean注入到SpittleController執行個體中,并設定MockMvc使用該執行個體。

不同于HomeControllerTest,該測試使用了

setSingleView()

,發起一個

/spittles

的GET請求,并斷言視圖是否為spittles以及model是否含有一個spittleList的屬性值。

當然,現在運作這個測試代碼肯定是會出錯的,因為還沒有SpittleController。

package spittr.web;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.hamcrest.core.IsCollectionContaining;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.view.InternalResourceView;

import spittr.Spittle;
import spittr.data.SpittleRepository;

public class SpittleControllerTest {

    @Test
    public void shouldShowRecentSpittles() throws Exception {
        List<Spittle> expectedSpittles = createSpittleList(20);
        SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
        Mockito.when(mockRepository.findSpittles(Long.MAX_VALUE, 20)).thenReturn(expectedSpittles);

        SpittleController controller = new SpittleController(mockRepository);
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setSingleView(new InternalResourceView("/WEB_INF/views/spittles.jsp")).build();
        
        // 調用MockMvc.perform(RequestBuilder requestBuilder)發起一個http請求,然後将得到ResultActions
        mockMvc.perform(MockMvcRequestBuilders.get("/spittles"))// 添加驗證斷言來判斷執行請求後的結果是否是預期的;
                .andExpect(MockMvcResultMatchers.view().name("spittles"))// view():得到視圖驗證器;
                // 得到相應的***ResultMatchers後,接着再調用其相應的API得到ResultMatcher,
                // 如ModelResultMatchers.attributeExists(final String... names)判斷Model屬性是否存在。
                .andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))// model():得到模型驗證器;
                .andExpect(MockMvcResultMatchers.model().attribute("spittleList", IsCollectionContaining.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 ", new Date()));
        }
        return spittles;
    }

}
           

SpittleController中,使用@Autowired注解注入了spittleRepository屬性。

需要注意的是

spittles()

方法使用了Model(控制器和視圖之間傳遞的資料)作為入參,Model本質上是一個map,它會被傳送至view,是以資料可以提供給用戶端。如果在調用

addAttribute()

方法時沒有指定key,那麼就會從傳入的對象中擷取,比如代碼中傳入的參數屬性是List<Spittle>,那麼key就是spittleList。最後,該方法傳回spittles作為傳動給model的視圖名稱。

也可以顯示的指定key:

model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
           

也可以直接采用map的方式:

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

不管采用何種方式實作spittles()方法,結果都是一樣的。一個Spittle對象集合會存儲在model中,并配置設定到名為spittles的view中,根據測試方法中的配置,該視圖就是/WEB-INF/views/spittles.jsp。

現在model已經有資料了,那麼JSP頁面中如何擷取資料呢?當視圖是一個JSP頁面時,model資料會作為請求屬性被拷貝到請求中,是以可以通過JSTL(JavaServer Pages Standard Tag Library)

<c:forEach>

來擷取:

<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進行擴充,讓它可以處理一些輸入。

接受輸入請求

Spring MVC提供了如下方式供用戶端傳遞資料到控制器處理方法:

  • Query parameters
  • Form parameters
  • Path variables

處理查詢參數:@RequestParam

Spittr應用的一個需求就是要對spittle清單分頁展示,但是SpittleController僅僅展示最近的spittle。如果要讓使用者可以每次得到一頁的spittle記錄,那麼就需要讓使用者可以通過某種方式将他們想看的spittle記錄的參數傳遞到背景。

在浏覽spittle時,如果想要檢視下一頁的spittle,那麼就需要傳遞比目前頁的最後一個spittle的id小一位的id,也可以傳遞想要展示的spittle的數量。

為了實作分頁,需要編寫一個控制器滿足:

  • before

    參數,結果中的spittle的id都要在這個參數之前;
  • count

    參數,結果中要包含的spittle的個數

下面我們對上面的

spittles()

方法進行小小的改動,讓它可以使用before和count參數。首先對測試方法進行改動:

@Test
    public void shouldShowRecentSpittles() throws Exception {
        List<Spittle> expectedSpittles = createSpittleList(20);
        SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
        Mockito.when(mockRepository.findSpittles(238900, 50)).thenReturn(expectedSpittles);

        SpittleController controller = new SpittleController(mockRepository);
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setSingleView(new InternalResourceView("/WEB_INF/views/spittles.jsp")).build();
        
        // 調用MockMvc.perform(RequestBuilder requestBuilder)發起一個http請求,然後将得到ResultActions
        mockMvc.perform(MockMvcRequestBuilders.get("/spittles?max=238900&count=50"))// 添加驗證斷言來判斷執行請求後的結果是否是預期的;
                .andExpect(MockMvcResultMatchers.view().name("spittles"))// view():得到視圖驗證器;
                // 得到相應的***ResultMatchers後,接着再調用其相應的API得到ResultMatcher,
                // 如ModelResultMatchers.attributeExists(final String... names)判斷Model屬性是否存在。
                .andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))// model():得到模型驗證器;
                .andExpect(MockMvcResultMatchers.model().attribute("spittleList", IsCollectionContaining.hasItems(expectedSpittles.toArray())));
    }
           

這個測試方法的主要改動就是它發起的GET請求傳遞了兩個參數:max和count。對

spittles()

進行修改:

@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參數沒有指定,那麼就會使用預設的設定。由于查詢參數是String類型的,是以

defaultValue

屬性值也需要設定為String類型,需要對Long.MAX_VALUE進行設定:

private static final String MAX_LONG_AS_STRING = "9223372036854775807";

雖然,這裡defaultValue的屬性為String類型,當運作到函數時,将會根據函數的參數類型進行轉換。

查詢參數是請求中傳送資訊給控制器的最常用方式,另外一種流行的方式就是将參數作為請求路徑的一部分。

通過路徑參數傳遞資料:@PathVariable

假設現在應用需要展示單獨的一篇Spittle,那麼就需要一個id作為查詢參數,對應的處理方法可以是:

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

這個handler方法将會處理形如

/spittles/show?spittle_id=12345

的請求,但是這并不符合資源導向的觀點。理想情況下,應該使用URL路徑對資源進行區分,而不是查詢參數,即應該使用

/spittles/12345

這種形式。

為了實作資源導向的控制器,我們先在測試中獲得這個需求(使用了靜态引入):

@Test
public void testSpittle() throws Exception {
    Spittle expectedSpittle = new Spittle("Hello", new Date());
    SpittleRepository mockRepository = Mockito.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));
}
           

該測試中發起了一個

/spittles/12345

的GET請求,并且對其傳回結果視圖進行斷言。

為了滿足路徑參數,Spring MVC允許在

@RequestMapping

路徑中使用占位符(需要用大括号包圍),下面是使用占位符來接受一個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")

注解,表明請求中占位符位置的值都會被傳送到handler的spittleId參數。@RequestMapping中value屬性的占位符必須和@PathVariable包裹的參數一緻。如果@PathVariable中沒有給定參數,那麼将預設使用入參的冊數參數名。即可以使用下面的方法:

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

spittle()方法會将接收的參數值傳遞給spittleRepository的findOne()方法并查找到一個Spittle,并将其放置到model中,model的key值會是spittle,接下來就可以在視圖中引用這個Spittle:

<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應用不僅僅是将内容推送給使用者,它同時也會讓使用者填寫表單并将資料送出給應用。

對于表單有兩種處理方式:展示表單以及處理使用者送出的表單資料。在Spittr中,需要提供一個供新使用者進行注冊的表單。

SpitterController

:展示使用者系統資料庫單

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

    // 處理來自/spitter/register的get請求
    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String showRegistrationForm() {
        return "registerForm";
    }

}
           

showRegistrationForm

方法的

@RequestMapping

注解,以及類級别的注解

@RequestMapping

,表明了這個方法會處理來自/spitter/register的get請求,該方法僅僅傳回了一個名為registerForm的邏輯視圖。根據之前在

InternalResourceViewResolver

中的配置,這個邏輯視圖會導向到

/WEB-INF/views/registerForm.jsp

該界面。

對應的測試方法:

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;

public class SpitterControllerTest {

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

也可以通過啟動項目通路界面的方式驗證:

<%@ 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/>
      Username: <input type="text" name="username" /><br/>
      Password: <input type="password" name="password" /><br/>
      <input type="submit" value="Register" />
    </form>
  </body>
</html>
           

該界面提供了使用者注冊的功能

接下來需要對送出的表單進行處理。

編寫表單處理控制器

在處理POST請求時,控制器需要接受表單資料并且将這些資料存儲為一個Spitter對象。為了避免重複送出,應該重定向到一個新的界面:使用者資訊頁。在處理post請求時,一個聰明的做法就是在處理完成後發送一個重定向的請求,進而可以避免重複送出。

下面來實作控制器方法,進而可以處理注冊請求。

private SpitterRepository spitterRepository;

    public SpitterController() {
    }

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

    public String processRegistration(Spitter spitter) {
        // 儲存Spitter
        spitterRepository.save(spitter);
        // 重定向到新的頁面
        return "redirect:/spitter/" + spitter.getUsername();
    }
           

processRegistration方法使用Spitter對象作為入參,該對象的屬性會從請求中填充。該方法中調用了spitterRepository的save方法對Spitter對象進行存儲。最後傳回了一個帶有

redirect:

的字元串。

當InternalResourceViewResolver遇到

redirect:

時,它會自動地将其當做一個重定向請求,進而可以重定向到使用者詳情頁面,如/spitter/xiaoming。

同時,InternalResourceViewResolver也可以識别字首

forward:

,這種情況下,請求會被轉向到給定的URL位址。

下面需要編寫處理處理使用者詳情頁面的方法:

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

參數校驗

從Spring3.0開始,Spring支援Java校驗api,進而可以進而可以不需要添加其他配置,僅僅需要有一個Java API 的實作,如Hibernate Validator。

Java Validation API定義了許多注解,可以使用這些注解來限制參數的值,所有的注解都在包

javax.validation.constraints

中。

注解 描述
@AssertFalse(@AssertTrue) 對象必須是布爾類型,并且必須為false(true)
@DecimalMax(value)、@DecimalMin(value) 限制對象必須是一個數字,其值不大于(不小于)指定的BigDecimalString值
@Digits(integer,fraction) 對象必須為一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Future 必須是一個将來的日期
@Max(value)、@Min(value) 必須為一個不大于(不小于)指定值的數字
@NotNull 限制對象不能為空
@Null 限制對象必須為空
@Past 必須是一個過去的日期
@Pattern(value) 必須符合指定的正規表達式
@Size(min,max) 限制字元長度必須在min到max之間

使用示例:

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

既然已經對Spitter的參數添加了限制,那麼就需要改動processRegistration方法來應用校驗:

@RequestMapping(value = "/register", method = RequestMethod.POST)
    public String processRegistration(@Valid Spitter spitter, Errors errors) {
        // 若校驗中出現錯誤,那麼就傳回到注冊界面
        if (errors.hasErrors()) {
            return "registerForm";
        }
        // 儲存Spitter
        spitterRepository.save(spitter);
        // 重定向到新的頁面
        return "redirect:/spitter/" + spitter.getUsername();
    }
           

總結

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

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

由于缺少真正的入庫操作,是以本章節的一些方法不能真正的運作。

在接下來的章節中,我們會對Spring視圖進行深入了解,對如何在JSP頁面中使用Spring标簽庫進行展開。

作者:hoxis

連結:https://www.jianshu.com/p/74357110e4cc

來源:簡書

簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。