JSP 和 JSTL

Spring 框架内置了将 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 是您要用于此库中标签的标签名前缀。

表单标签

此标签渲染一个 HTML 'form' 元素,并为内部标签公开绑定路径以进行绑定。它将命令对象放入PageContext 中,以便内部标签可以访问命令对象。此库中的所有其他标签都是form 标签的嵌套标签。

假设我们有一个名为User 的域对象。它是一个 JavaBean,具有诸如firstNamelastName 之类的属性。我们可以将其用作表单控制器(返回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'。有关此标签的示例,请参见表单标签。您还可以使用 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 标签的用途。您可以传递一个 Array、一个 List 或一个 Map,其中包含 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,则地图条目键用作值,地图条目的值用作要显示的标签。您还可以使用自定义对象,您可以在其中使用 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 标签。您传入一个 Array、一个 List 或一个 Map,其中包含 items 属性中的可用选项。如果您使用 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 的技能是草药学,则 'Skills' 行的 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 的房子在格兰芬多,则 'House' 行的 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 的项目,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果 itemValueitemLabel(或两者)恰好被指定,则项目值属性应用于映射键,项目标签属性应用于映射值。

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,并带有绑定值。要提交未绑定的隐藏值,请使用 HTML input 标签,其 type 设置为 hidden。以下 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,或者你可以使用“真实”方法作为附加参数(在 HTML 表单中建模为隐藏输入字段)来执行 POST。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 方法隐藏在请求参数后面。它由 HiddenHttpMethodFilter 捕获,该过滤器在 web.xml 中定义,如下例所示

<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 之外的类型属性。这旨在允许渲染新的 HTML5 特定输入类型,例如 emaildaterange 等。请注意,输入 type='text' 不是必需的,因为 text 是默认类型。