JSP and JSTL
Spring Framework 为将 Spring MVC 与 JSP 和 JSTL 一起使用提供了内置集成。
View Resolvers
在使用 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>
Spring’s JSP Tag Library
如前几章所述,Spring 提供了将请求参数数据绑定到命令对象的功能。为了促进 JSP 页面的开发与那些数据绑定功能的结合,Spring 提供了几个标签,让事情变得更简单。所有 Spring 标签都具有 HTML 转义功能,可以启用或禁用字符的转义。
`spring.tld`标记库描述符(TLD)包含在 `spring-webmvc.jar`中。要获得各个标记的全面参考,请浏览 API 参考 或查看标记库描述。
Spring’s form tag library
从版本 2.0 开始,Spring 提供了一组全面的数据绑定感知标签,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都为其对应的 HTML 标记对应项的属性集提供支持,使这些标记很熟悉且直观。标签生成的 HTML 是 HTML 4.01/XHTML 1.0 兼容的。
与其他表单/输入标记库不同,Spring 的表单标记库与 Spring Web MVC 集成,使这些标记能够访问您的控制器所处理的命令对象和参考数据。正如我们在以下示例中所示,表单标记使 JSP 更易于开发、阅读和维护。
我们将详细了解这些表单标记,并看一个示例来说明如何使用每个标记。我们已经包含了生成的 HTML 代码段,而某些标记需要进一步说明。
Configuration
表单标记库捆绑在 spring-webmvc.jar
中。库描述符称为 spring-form.tld
。
要使用此库中的标记,请将以下指令添加到 JSP 页面的顶部:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中,form
是您想在此库中用于这些标记的标记名称前缀。
The Form Tag
这个标记呈现一个 HTML “form”元素,并为用于绑定的内部标记公开一个绑定路径。它将命令对象放入 PageContext
中,以便内部标记可以访问该命令对象。此库中的所有其他标记都是 form
标记的嵌套标记。
假设我们有一个名为 User
的域对象。它是一个包含诸如 firstName
和 lastName
之类的属性的 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>
firstName
和 lastName
值是从页控制器放置在 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>
The input
Tag
此标签会默认渲染带有绑定值和`type='text'`的 HTML input`元素。有关此标签的示例,请参阅The Form Tag。你还可以使用 HTML5 特定的类型,例如`email
、tel
、`date`等。
The checkbox
Tag
此标记呈现一个 type
设置为 checkbox
的 HTML input
标记。
假设我们的`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>
有 3 种方法来使用`checkbox`标签,这应该能满足你对复选框的所有需求。
-
方法一:当绑定的值是
java.lang.Boolean
类型时,如果绑定的值是true
,则input(checkbox)
标记为checked
。value
属性对应于setValue(Object)
值属性的已解析值。 -
方法二:当绑定的值是
array
或java.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 “复选框在表单中可见,我希望我的对象(表单数据绑定到其上)反映复选框的状态,无论其状态如何.
”
The checkboxes
Tag
此标签呈现出多个 HTML input`标签,其中`type`设置为`checkbox
。
本节基于前一个`checkbox`标签部分的示例。有时,你可能不喜欢在 JSP 页面中列出所有可能的爱好。你更愿意在运行时为可用选项提供一个列表并将其传递到标签中。这就是`checkboxes`标签的用途。你可以在`items`属性中传入包含可用选项的`Array`、List`或`Map
。通常,绑定属性是一个集合,以便它可以保存用户选择的多个值。以下示例显示了一个使用此标签的 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`为标签提供属性名。
The radiobutton
Tag
此标签呈现出一个 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>
The radiobuttons
Tag
此标签呈现出多个 HTML input`元素,其中`type`设置为`radio
。
与 xref:web/webmvc-view/mvc-jsp.adoc#mvc-view-jsp-formtaglib-checkboxestag[checkboxes
标签一样,你可能希望将可用选项作为运行时变量传递。对于此用法,你可以使用 radiobuttons
标签。你需要传递一个 Array
、List
或 Map
,其中包含 items
属性中的可用选项。如果你使用 Map
,则映射项的键用作值,而映射项的值用作显示的标签。你还可以使用一个自定义对象,通过使用 itemValue
提供值属性,通过使用 itemLabel
提供标签,如下例所示:
<tr>
<td>Sex:</td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
The password
Tag
此标签呈现出一个 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>
The select
Tag
此标签呈现一个 HTML 'select' 元素。它支持与所选选项以及嵌套`option`和`options`标签的使用进行数据绑定。
假设`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>
The option
Tag
此标签呈现一个 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 属性已添加。 |
The options
Tag
此标签会呈现一个 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
住在英国,Country
行的 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
属性通常会使用项目对象集合或数组来填充。如果指定了 itemValue
和 itemLabel
,则它们将指代这些项目对象的 bean 属性。否则,项目对象本身会被转换为字符串。或者,你可以指定一个项目的 Map
,在这种情况下,map 密钥会被解释为选项值,map 值对应于选项标签。如果也碰巧指定了 itemValue
或 itemLabel
(或两者),则项目值属性会应用于 map 密钥,项目标签属性会应用于 map 值。
The textarea
Tag
此标签会呈现一个 HTML textarea
元素。以下 HTML 显示了它的典型输出:
<tr>
<td>Notes:</td>
<td><form:textarea path="notes" rows="3" cols="20"/></td>
<td><form:errors path="notes"/></td>
</tr>
The hidden
Tag
此标签会呈现一个 HTML input
标签,其中 type
被设置为带绑定值 的 hidden
。要提交一个未绑定的隐藏值,请使用带 type
被设置为 hidden
的 HTML input
标签。以下 HTML 显示了它的典型输出:
<form:hidden path="house"/>
如果我们选择将 house
值提交为一个隐藏值,则 HTML 如下所示:
<input name="house" type="hidden" value="Gryffindor"/>
The errors
Tag
此标签会将字段错误呈现到一个 HTML span
元素中。它提供了对在控制器中创建的错误或与控制器关联的任何验证器创建的错误的访问。
假设我们想要在提交表单后显示 firstName
和 lastName
字段的所有错误消息。我们有针对 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>
如果我们提交一个在 firstName
和 lastName
字段中带有空值的表单,则 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="*"
: Displays all errors. -
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 Method Conversion
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"
}