JSP和JSTL
Spring框架内置了将Spring MVC与JSP和JSTL结合使用的集成功能。
视图解析器
在使用JSP进行开发时,通常会声明一个InternalResourceViewResolver bean。
InternalResourceViewResolver 可用于调度到任何 Servlet 资源,但特别适用于 JSP 文件。作为最佳实践,我们强烈建议将 JSP 文件放置在 WEB-INF 目录下的某个子目录中,这样客户端就无法直接访问这些文件了。
以下配置所做的就是注册一个JSP视图解析器,该解析器使用默认的视图名称前缀“/WEB-INF/”和默认的后缀“.jsp”。
- Java
- Kotlin
- Xml
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp();
}
}
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.jsp()
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:view-resolvers>
<mvc:jsp/>
</mvc:view-resolvers>
</beans>
你可以指定自定义的前缀和后缀。
JSPs与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代码片段。
配置
form标签库包含在spring-webmvc.jar中。该库的描述文件名为spring-form.tld。
要使用此库中的标签,请在JSP页面的顶部添加以下指令:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中 form 是你希望用于此库中标签的标签前缀名称。
Form 标签
此标签渲染一个HTML的“form”元素,并为内部标签提供绑定路径以便进行数据绑定。它将命令对象放入PageContext中,这样内部标签就可以访问该命令对象了。这个库中的所有其他标签都是“form”标签的嵌套标签。
假设我们有一个名为User的领域对象(domain object)。它是一个JavaBean,具有firstName和lastName等属性。我们可以将其用作表单控制器(form controller)的表单支撑对象(form-backing object),该控制器返回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 的值是从页面控制器(page controller)放入 PageContext 中的命令对象(command object)中获取的。继续阅读,以了解如何更复杂地使用 form 标签与内部标签(inner tags)结合的示例。
以下列表显示了生成的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 元素,默认情况下该元素的值为绑定值(bound value),类型为 text。有关此标签的示例,请参阅 表单标签。您还可以使用HTML5特有的输入类型,如 email、tel、date 等。
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)会被标记为选中状态。value属性对应于setValue(Object)属性解析后的值。 -
方法二:当绑定值为
array或java.util.Collection类型时,如果配置的setValue(Object)值存在于绑定的Collection中,则input(checkbox)会被标记为选中状态。 -
方法三:对于其他任何类型的绑定值,如果配置的
setValue(Object)与绑定值相等,则input(checkbox)会被标记为选中状态。
请注意,无论采用哪种方法,都会生成相同的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请求参数的一部分被发送到服务器。因此,为了使Spring表单数据绑定能够正常工作,我们需要解决HTML中的这一特殊问题。checkbox标签遵循了Spring的现有规范,为每个复选框添加一个以下划线(_)开头的隐藏参数。通过这种方式,你实际上是在告诉Spring:“这个复选框在表单中是可见的,无论怎样,我都希望绑定表单数据的对象能够反映出该复选框的状态。”
checkboxes 标签
此标签会渲染多个HTML input 标签,其 type 属性被设置为 checkbox。
本节基于之前“checkbox”标签部分的示例进行扩展。有时候,你可能不希望在JSP页面中列出所有可能的爱好选项。你更希望能够在运行时提供可用的选项列表,并将其传递给该标签。这就是“checkboxes”标签的用途。你可以通过“items”属性传递一个包含可用选项的“Array”、“List”或“Map”。通常,绑定属性是一个集合(collection),以便它可以容纳用户选择的多个值。以下示例展示了一个使用此标签的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标签。您可以通过items属性传递一个包含可用选项的Array、List或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”元素。它支持与所选选项的数据绑定,同时也支持使用嵌套的“option”和“options”标签。
假设一个User(用户)有一系列技能。相应的HTML代码可能如下所示:
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}"/></td>
</tr>
如果用户的技能是“草药学”,那么“技能”这一行的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>
如果“用户”的学院属于格兰芬多(Gryffindor),那么‘学院’这一行的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>
注意添加了
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>
如果“用户”居住在英国,那么“国家”这一行的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>
注意新增了一个
selected属性。
如前面的例子所示,将option标签与options标签结合使用可以生成相同的标准HTML,但允许你在JSP中明确指定仅用于显示的值(即该值出现的位置),例如例子中的默认字符串:“-- 请选择”。
items属性通常填充有一组项目对象(item objects)的集合或数组。如果指定了itemValue和itemLabel,它们就分别对应这些项目对象的bean属性;否则,这些项目对象本身会被转换为字符串。另外,你也可以指定一个Map来存储项目信息,在这种情况下,Map的键会被解释为选项值(option values),而Map的值则对应于选项标签(option labels)。如果同时指定了itemValue或itemLabel(或两者都有),那么itemValue属性将应用于Map的键,而itemLabel属性将应用于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 标签
此标签会渲染一个type属性设置为hidden的HTML input标签,并绑定相应的值。如果要提交一个未绑定的隐藏值,也可以使用type属性设置为hidden的HTML input标签。以下HTML代码展示了其典型的输出效果:
<form:hidden path="house"/>
如果我们选择将house值作为隐藏字段提交,HTML代码将如下所示:
<input name="house" type="hidden" value="Gryffindor"/>
errors 标签
此标签会在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="*":显示所有错误。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的表单标签(form tag)已经更新为可以设置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 Form 标签库允许输入动态属性,这意味着你可以输入任何 HTML5 特有的属性。
input 标签支持输入除 text 之外的其他类型属性。这样就可以渲染新的 HTML5 特定输入类型,如 email、date、range 等。需要注意的是,不一定要输入 type='text',因为 text 是默认类型。