JSP 和 JSTL

Spring Framework 內建了將 Spring MVC 與 JSP 和 JSTL 整合的支援。

檢視解析器

使用 JSP 進行開發時,通常會宣告一個 InternalResourceViewResolver bean。

InternalResourceViewResolver 可用於分發到任何 Servlet 資源,特別是 JSP。作為最佳實踐,我們強烈建議將 JSP 檔案放在 'WEB-INF' 目錄下的某個子目錄中,以便客戶端無法直接訪問。

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
	<property name="prefix" value="/WEB-INF/jsp/"/>
	<property name="suffix" value=".jsp"/>
</bean>

JSP 與 JSTL 對比

使用 JSP 標準標籤庫 (JSTL) 時,必須使用特殊的檢視類 JstlView,因為 JSTL 需要一些準備工作才能使 I18N 等功能正常工作。

Spring 的 JSP 標籤庫

Spring 提供請求引數到命令物件的資料繫結,如前面章節所述。為了方便將 JSP 頁面與這些資料繫結功能結合開發,Spring 提供了幾個標籤,使事情變得更加容易。所有 Spring 標籤都具有 HTML 轉義功能,可以啟用或停用字元轉義。

spring.tld 標籤庫描述符 (TLD) 包含在 spring-webmvc.jar 中。要檢視單個標籤的詳細參考,請查閱API參考文件或檢視標籤庫描述。

Spring 的表單標籤庫

從版本 2.0 開始,Spring 提供了一套全面的資料繫結感知標籤,用於在使用 JSP 和 Spring Web MVC 時處理表單元素。每個標籤都支援與其對應的 HTML 標籤屬性集,使得這些標籤熟悉且易於使用。標籤生成的 HTML 符合 HTML 4.01/XHTML 1.0 標準。

與其他表單/輸入標籤庫不同,Spring 的表單標籤庫與 Spring Web MVC 整合,使標籤能夠訪問你的控制器處理的命令物件和參考資料。如以下示例所示,表單標籤使得 JSP 的開發、閱讀和維護更加容易。

我們將逐一介紹表單標籤,並檢視每個標籤的使用示例。對於需要進一步說明的標籤,我們還包含了生成的 HTML 片段。

配置

表單標籤庫捆綁在 spring-webmvc.jar 中。標籤庫描述符名為 spring-form.tld

要使用此庫中的標籤,請將以下指令新增到 JSP 頁面的頂部:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

其中 form 是你希望為此標籤庫中的標籤使用的標籤名字首。

form 標籤

此標籤渲染一個 HTML 'form' 元素,並向內部標籤暴露繫結路徑以進行繫結。它將命令物件放入 PageContext 中,以便內部標籤可以訪問該命令物件。此庫中的所有其他標籤都是 form 標籤的巢狀標籤。

假設我們有一個名為 User 的域物件。它是一個包含 firstNamelastName 等屬性的 JavaBean。我們可以將其用作表單控制器的表單支援物件,該控制器返回 form.jsp。以下示例展示了 form.jsp 的樣子:

<form:form>
	<table>
		<tr>
			<td>First Name:</td>
			<td><form:input path="firstName"/></td>
		</tr>
		<tr>
			<td>Last Name:</td>
			<td><form:input path="lastName"/></td>
		</tr>
		<tr>
			<td colspan="2">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form:form>

firstNamelastName 的值是從頁面控制器放置在 PageContext 中的命令物件中檢索的。繼續閱讀以檢視更復雜的示例,瞭解如何在 form 標籤中使用內部標籤。

以下列表展示了生成的 HTML,看起來像一個標準表單:

<form method="POST">
	<table>
		<tr>
			<td>First Name:</td>
			<td><input name="firstName" type="text" value="Harry"/></td>
		</tr>
		<tr>
			<td>Last Name:</td>
			<td><input name="lastName" type="text" value="Potter"/></td>
		</tr>
		<tr>
			<td colspan="2">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form>

前面的 JSP 假設表單支援物件的變數名為 command。如果將表單支援物件放在模型中並使用其他名稱(這絕對是最佳實踐),可以將表單繫結到指定名稱的變數,如下例所示:

<form:form modelAttribute="user">
	<table>
		<tr>
			<td>First Name:</td>
			<td><form:input path="firstName"/></td>
		</tr>
		<tr>
			<td>Last Name:</td>
			<td><form:input path="lastName"/></td>
		</tr>
		<tr>
			<td colspan="2">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form:form>

input 標籤

此標籤預設渲染一個 HTML input 元素,帶有繫結值和 type='text'。有關此標籤的示例,請參見form標籤。你還可以使用 HTML5 特定的型別,例如 emailteldate 等。

checkbox 標籤

此標籤渲染一個 HTML input 標籤,其 type 設定為 checkbox

假設我們的 User 具有諸如訂閱新聞郵件和一系列愛好等偏好。以下示例展示了 Preferences 類:

  • Java

  • Kotlin

public class Preferences {

	private boolean receiveNewsletter;
	private String[] interests;
	private String favouriteWord;

	public boolean isReceiveNewsletter() {
		return receiveNewsletter;
	}

	public void setReceiveNewsletter(boolean receiveNewsletter) {
		this.receiveNewsletter = receiveNewsletter;
	}

	public String[] getInterests() {
		return interests;
	}

	public void setInterests(String[] interests) {
		this.interests = interests;
	}

	public String getFavouriteWord() {
		return favouriteWord;
	}

	public void setFavouriteWord(String favouriteWord) {
		this.favouriteWord = favouriteWord;
	}
}
class Preferences(
		var receiveNewsletter: Boolean,
		var interests: StringArray,
		var favouriteWord: String
)

相應的 form.jsp 可能如下所示:

<form:form>
	<table>
		<tr>
			<td>Subscribe to newsletter?:</td>
			<%-- Approach 1: Property is of type java.lang.Boolean --%>
			<td><form:checkbox path="preferences.receiveNewsletter"/></td>
		</tr>

		<tr>
			<td>Interests:</td>
			<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
			<td>
				Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
				Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
				Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
			</td>
		</tr>

		<tr>
			<td>Favourite Word:</td>
			<%-- Approach 3: Property is of type java.lang.Object --%>
			<td>
				Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
			</td>
		</tr>
	</table>
</form:form>

checkbox 標籤有三種用法,應該能滿足你所有的複選框需求。

  • 第一種方法:當繫結值的型別是 java.lang.Boolean 時,如果繫結值為 true,則 input(checkbox) 會被標記為 checkedvalue 屬性對應於 setValue(Object) 值屬性的解析值。

  • 第二種方法:當繫結值的型別是 arrayjava.util.Collection 時,如果配置的 setValue(Object) 值存在於繫結的 Collection 中,則 input(checkbox) 會被標記為 checked

  • 第三種方法:對於任何其他繫結值型別,如果配置的 setValue(Object) 等於繫結值,則 input(checkbox) 會被標記為 checked

注意,無論採用哪種方法,都會生成相同的 HTML 結構。以下 HTML 片段定義了一些複選框:

<tr>
	<td>Interests:</td>
	<td>
		Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
		<input type="hidden" value="1" name="_preferences.interests"/>
		Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
		<input type="hidden" value="1" name="_preferences.interests"/>
		Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/>
		<input type="hidden" value="1" name="_preferences.interests"/>
	</td>
</tr>

你可能沒想到會在每個複選框後面看到額外的隱藏欄位。當 HTML 頁面中的複選框未被選中時,提交表單後其值不會作為 HTTP 請求引數的一部分發送到伺服器,因此我們需要對 HTML 中的這個特性進行變通,以便 Spring 表單資料繫結能夠正常工作。checkbox 標籤遵循了 Spring 現有的約定,即為每個複選框包含一個以下劃線(_)為字首的隱藏引數。透過這樣做,你實際上是告訴 Spring:“這個複選框在表單中是可見的,我希望表單資料繫結的物件能夠反映複選框的狀態,無論它是否被選中。”

checkboxes 標籤

此標籤渲染多個 HTML input 標籤,其 type 設定為 checkbox

本節基於上一節checkbox 標籤的示例。有時,你可能不希望在 JSP 頁面中列出所有可能的愛好。你寧願在執行時提供可用選項的列表並將其傳遞給標籤。這就是 checkboxes 標籤的作用。你可以傳遞一個 ArrayListMap,其中包含 items 屬性中可用的選項。通常,繫結屬性是一個集合,以便它可以儲存使用者選擇的多個值。以下示例展示了一個使用此標籤的 JSP:

<form:form>
	<table>
		<tr>
			<td>Interests:</td>
			<td>
				<%-- Property is of an array or of type java.util.Collection --%>
				<form:checkboxes path="preferences.interests" items="${interestList}"/>
			</td>
		</tr>
	</table>
</form:form>

此示例假設 interestList 是一個 List,作為模型屬性可用,其中包含要選擇的值的字串。如果你使用 Map,則 Map 條目的鍵用作值,而 Map 條目的值用作要顯示的標籤。你還可以使用自定義物件,透過使用 itemValue 提供值的屬性名,並透過使用 itemLabel 提供標籤的屬性名。

radiobutton 標籤

此標籤渲染一個 HTML input 元素,其 type 設定為 radio

典型的使用模式涉及繫結到同一屬性但具有不同值的多個標籤例項,如下例所示:

<tr>
	<td>Sex:</td>
	<td>
		Male: <form:radiobutton path="sex" value="M"/> <br/>
		Female: <form:radiobutton path="sex" value="F"/>
	</td>
</tr>

radiobuttons 標籤

此標籤渲染多個 HTML input 元素,其 type 設定為 radio

checkboxes 標籤一樣,你可能希望將可用選項作為執行時變數傳遞。對於這種用法,你可以使用 radiobuttons 標籤。你可以傳遞一個 ArrayListMap,其中包含 items 屬性中可用的選項。如果你使用 Map,則 Map 條目的鍵用作值,而 Map 條目的值用作要顯示的標籤。你還可以使用自定義物件,透過使用 itemValue 提供值的屬性名,並透過使用 itemLabel 提供標籤的屬性名,如下例所示:

<tr>
	<td>Sex:</td>
	<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>

password 標籤

此標籤渲染一個 HTML input 標籤,其型別設定為 password,帶有繫結值。

<tr>
	<td>Password:</td>
	<td>
		<form:password path="password"/>
	</td>
</tr>

請注意,預設情況下,密碼值不會顯示。如果你確實希望顯示密碼值,可以將 showPassword 屬性的值設定為 true,如下例所示:

<tr>
	<td>Password:</td>
	<td>
		<form:password path="password" value="^76525bvHGq" showPassword="true"/>
	</td>
</tr>

select 標籤

此標籤渲染一個 HTML 'select' 元素。它支援對選定選項進行資料繫結,也支援使用巢狀的 optionoptions 標籤。

假設一個 User 有一系列技能。相應的 HTML 可能如下所示:

<tr>
	<td>Skills:</td>
	<td><form:select path="skills" items="${skills}"/></td>
</tr>

如果 User 的技能是藥草學,則“技能”行的 HTML 原始碼可能如下所示:

<tr>
	<td>Skills:</td>
	<td>
		<select name="skills" multiple="true">
			<option value="Potions">Potions</option>
			<option value="Herbology" selected="selected">Herbology</option>
			<option value="Quidditch">Quidditch</option>
		</select>
	</td>
</tr>

option 標籤

此標籤渲染一個 HTML option 元素。它根據繫結值設定 selected 屬性。以下 HTML 顯示了典型的輸出:

<tr>
	<td>House:</td>
	<td>
		<form:select path="house">
			<form:option value="Gryffindor"/>
			<form:option value="Hufflepuff"/>
			<form:option value="Ravenclaw"/>
			<form:option value="Slytherin"/>
		</form:select>
	</td>
</tr>

如果 User 的學院是格蘭芬多,則“學院”行的 HTML 原始碼可能如下所示:

<tr>
	<td>House:</td>
	<td>
		<select name="house">
			<option value="Gryffindor" selected="selected">Gryffindor</option> (1)
			<option value="Hufflepuff">Hufflepuff</option>
			<option value="Ravenclaw">Ravenclaw</option>
			<option value="Slytherin">Slytherin</option>
		</select>
	</td>
</tr>
1 請注意添加了 selected 屬性。

options 標籤

此標籤渲染一個 HTML option 元素列表。它根據繫結值設定 selected 屬性。以下 HTML 顯示了典型的輸出:

<tr>
	<td>Country:</td>
	<td>
		<form:select path="country">
			<form:option value="-" label="--Please Select"/>
			<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
		</form:select>
	</td>
</tr>

如果 User 居住在英國,則“國家”行的 HTML 原始碼可能如下所示:

<tr>
	<td>Country:</td>
	<td>
		<select name="country">
			<option value="-">--Please Select</option>
			<option value="AT">Austria</option>
			<option value="UK" selected="selected">United Kingdom</option> (1)
			<option value="US">United States</option>
		</select>
	</td>
</tr>
1 請注意添加了 selected 屬性。

如前面的示例所示,option 標籤與 options 標籤結合使用,會生成相同的標準 HTML,但允許你在 JSP 中明確指定一個僅用於顯示的值(它應該屬於的地方),例如示例中的預設字串:“-- 請選擇”。

items 屬性通常填充一個包含專案物件的集合或陣列。如果指定了 itemValueitemLabel,它們引用這些專案物件的 bean 屬性。否則,專案物件本身將轉換為字串。或者,你可以指定一個專案的 Map,在這種情況下,Map 鍵被解釋為選項值,而 Map 值對應於選項標籤。如果同時也指定了 itemValueitemLabel(或兩者),則專案值屬性適用於 Map 鍵,而專案標籤屬性適用於 Map 值。

textarea 標籤

此標籤渲染一個 HTML textarea 元素。以下 HTML 顯示了典型的輸出:

<tr>
	<td>Notes:</td>
	<td><form:textarea path="notes" rows="3" cols="20"/></td>
	<td><form:errors path="notes"/></td>
</tr>

hidden 標籤

此標籤渲染一個 HTML input 標籤,其 type 設定為 hidden,帶有繫結值。要提交未繫結(非資料繫結)的隱藏值,請使用 type 設定為 hidden 的 HTML input 標籤。以下 HTML 顯示了典型的輸出:

<form:hidden path="house"/>

如果我們選擇將 house 值作為隱藏值提交,HTML 將如下所示:

<input name="house" type="hidden" value="Gryffindor"/>

errors 標籤

此標籤在 HTML span 元素中渲染欄位錯誤。它提供了訪問你的控制器中建立的錯誤或與你的控制器關聯的任何校驗器建立的錯誤的功能。

假設我們希望在提交表單後顯示 firstNamelastName 欄位的所有錯誤訊息。我們有一個用於 User 類例項的校驗器,名為 UserValidator,如下例所示:

  • Java

  • Kotlin

public class UserValidator implements Validator {

	public boolean supports(Class candidate) {
		return User.class.isAssignableFrom(candidate);
	}

	public void validate(Object obj, Errors errors) {
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
	}
}
class UserValidator : Validator {

	override fun supports(candidate: Class<*>): Boolean {
		return User::class.java.isAssignableFrom(candidate)
	}

	override fun validate(obj: Any, errors: Errors) {
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.")
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.")
	}
}

form.jsp 可能如下所示:

<form:form>
	<table>
		<tr>
			<td>First Name:</td>
			<td><form:input path="firstName"/></td>
			<%-- Show errors for firstName field --%>
			<td><form:errors path="firstName"/></td>
		</tr>

		<tr>
			<td>Last Name:</td>
			<td><form:input path="lastName"/></td>
			<%-- Show errors for lastName field --%>
			<td><form:errors path="lastName"/></td>
		</tr>
		<tr>
			<td colspan="3">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form:form>

如果我們在 firstNamelastName 欄位中提交空值,HTML 將如下所示:

<form method="POST">
	<table>
		<tr>
			<td>First Name:</td>
			<td><input name="firstName" type="text" value=""/></td>
			<%-- Associated errors to firstName field displayed --%>
			<td><span name="firstName.errors">Field is required.</span></td>
		</tr>

		<tr>
			<td>Last Name:</td>
			<td><input name="lastName" type="text" value=""/></td>
			<%-- Associated errors to lastName field displayed --%>
			<td><span name="lastName.errors">Field is required.</span></td>
		</tr>
		<tr>
			<td colspan="3">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form>

如果我們想顯示給定頁面的完整錯誤列表怎麼辦?下一個示例顯示 errors 標籤也支援一些基本的萬用字元功能。

  • path="*": 顯示所有錯誤。

  • path="lastName": 顯示與 lastName 欄位關聯的所有錯誤。

  • 如果省略 path,則只顯示物件錯誤。

以下示例在頁面頂部顯示錯誤列表,然後在欄位旁邊顯示特定於欄位的錯誤

<form:form>
	<form:errors path="*" cssClass="errorBox"/>
	<table>
		<tr>
			<td>First Name:</td>
			<td><form:input path="firstName"/></td>
			<td><form:errors path="firstName"/></td>
		</tr>
		<tr>
			<td>Last Name:</td>
			<td><form:input path="lastName"/></td>
			<td><form:errors path="lastName"/></td>
		</tr>
		<tr>
			<td colspan="3">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form:form>

HTML 如下所示

<form method="POST">
	<span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
	<table>
		<tr>
			<td>First Name:</td>
			<td><input name="firstName" type="text" value=""/></td>
			<td><span name="firstName.errors">Field is required.</span></td>
		</tr>

		<tr>
			<td>Last Name:</td>
			<td><input name="lastName" type="text" value=""/></td>
			<td><span name="lastName.errors">Field is required.</span></td>
		</tr>
		<tr>
			<td colspan="3">
				<input type="submit" value="Save Changes"/>
			</td>
		</tr>
	</table>
</form>

spring-form.tld 標籤庫描述符 (TLD) 包含在 spring-webmvc.jar 中。有關單個標籤的完整參考,請瀏覽 API 參考 或檢視標籤庫描述。

HTTP 方法轉換

REST 的一個關鍵原則是使用“統一介面”。這意味著所有資源(URL)都可以透過使用相同的四種 HTTP 方法進行操作:GET、PUT、POST 和 DELETE。對於每種方法,HTTP 規範都定義了精確的語義。例如,GET 應該始終是安全操作,這意味著它沒有副作用;而 PUT 或 DELETE 應該是冪等的,這意味著你可以重複這些操作,但最終結果應該相同。雖然 HTTP 定義了這四種方法,但 HTML 只支援兩種:GET 和 POST。幸運的是,有兩種可能的變通方法:你可以使用 JavaScript 執行 PUT 或 DELETE,或者你可以使用 POST,並將“實際”方法作為附加引數(在 HTML 表單中建模為隱藏輸入欄位)。Spring 的 HiddenHttpMethodFilter 使用了後一種技巧。此過濾器是一個普通的 Servlet 過濾器,因此可以與任何 Web 框架結合使用(不僅限於 Spring MVC)。將此過濾器新增到你的 web.xml,並且帶有隱藏 method 引數的 POST 請求將被轉換為相應的 HTTP 方法請求。

為了支援 HTTP 方法轉換,Spring MVC 表單標籤已更新,以支援設定 HTTP 方法。例如,以下程式碼片段來自 Pet Clinic 示例

<form:form method="delete">
	<p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>

前面的示例執行 HTTP POST,其中“實際”的 DELETE 方法隱藏在一個請求引數後面。它由定義在 web.xml 中的 HiddenHttpMethodFilter 接收處理,如下例所示

<filter>
	<filter-name>httpMethodFilter</filter-name>
	<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
	<filter-name>httpMethodFilter</filter-name>
	<servlet-name>petclinic</servlet-name>
</filter-mapping>

以下示例顯示了相應的 @Controller 方法

  • Java

  • Kotlin

@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
	this.clinic.deletePet(petId);
	return "redirect:/owners/" + ownerId;
}
@RequestMapping(method = [RequestMethod.DELETE])
fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {
	clinic.deletePet(petId)
	return "redirect:/owners/$ownerId"
}

HTML5 標籤

Spring 表單標籤庫允許輸入動態屬性,這意味著你可以輸入任何 HTML5 特定的屬性。

表單的 input 標籤支援輸入 text 以外的 type 屬性。這旨在允許渲染新的 HTML5 特定的輸入型別,例如 emaildaterange 等。請注意,輸入 type='text' 不是必需的,因為 text 是預設型別。