理解视图解析
将控制器中的请求处理的逻辑和视图的渲染解耦合是Spring MVC的一个重要特性。如果控制器直接负责产生HTML,那么就很难不影响处理逻辑的前提下,维护和更新视图。控制器方法和视图的实现会在模型内容上达成一致,这是两者最大的关联。除此之外,应该两者应该保持足够的距离实现解耦合。
在之前,我们使用了名为InternalResourceViewResolver的视图解析器,在它的配置中,我们使用“/WEB-INF/views”和".jsp"的前后缀来确定JSP的物理位置。现在,我们回过来看一下视图解析的基础知识,以及Spring提供的其他视图解析器。
SpringMVC定义了一个名为ViewResolver的接口,它大致如下所示:
public interface ViewResolver{
View resolveViewName(String viewName,Locale locale)
throws Exception;
}
给resolveViewName()方法传入一个视图名和Locale对象时,它会返回一个View实例,View是另外一个接口,如下所示:
public interface View{
String getContentType();
void render(Map<String,?> model,
HttpServletRequest request,
HttpServletResponse,response)throws Exception;
}
View 接口的任务就是接受模型以及Servlet的request和response对象,并将输出结果渲染到response中。
当然我们并不需要自己做这几个实现,Spring提供了多个内置实现:
创建JSP视图
配置适用于JSP的视图解析器
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
解析JSTL视图
到目前为止,我们对InternalResourceViewResolver 的配置都很基础和简单,它最终会将逻辑视图名解析为InternalResourceView的实例,这个实例会引用jsp文件。但如果这些jsp使用JSTL标签来处理格式化和信息的话,我们会希望InternalResourceViewResolver 将视图解析为JstlView。
如果想让InternalResourceViewResolver 将视图解析为JstView而不是InternalResourceView,我们只需设置它的viewClass属性即可。
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);
return resolver;
}
使用Spring的JSP库
为了避免在脚本块中直接编写java代码,Spring提供了两个JSP标签库,用来帮助定义Spring MVC Web的视图。其中一个标签库会用来渲染HTML表单标签,这些标签可以绑定model中的某个属性。另外一个标签包含了一些工具类标签。
- 将表单绑定到模型上
Spring的表单绑定JSP标签库包含了14个标签,为了使用表单绑定库,需要在JSP页面中对其进行声明。
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
我们也可以选择其他前缀,如"form"等等,但是sf很简洁,易于输入。
这样,我们就可以使用14个相关的标签了:
所以我们这样写:
修改前:
<form method="POST">
First Name: <input type="text" name="firstName" /><br/>
Last Name: <input type="text" name="lastName" /><br/>
Email: <input type="email" name="email" /><br/>
Username: <input type="text" name="username" /><br/>
Password: <input type="password" name="password" /><br/>
<input type="submit" value="Register" />
</form>
修改后:
<sf:form method="POST" commandName="spitter">
First Name: <sf:input path="firstName" /><br/>
Last Name: <sf:input path="lastName" /><br/>
Email: <sf:input path="email" /><br/>
Username: <sf:input path="username" /><br/>
Password: <sf:password path="password" /><br/>
<input type="submit" value="Register" />
</sf:form>
这里用<sf:form>
替代了<form>
,它会通过commandName属性构建针对某个模型model对象的上下文信息。commandName设置为spitter,就是model中必须要有一个key为spitter的对象,否则表单不能正常渲染,会出现jsp错误。为此,我们必须修改刚刚进入这个界面的内容:
修改前:
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String showRegitrationForm() {
return "registerForm";
}
修改后:
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String showRegitrationForm(Model model) {
model.addAttribute(new Spitter());
return "registerForm";
}
修改后的方法中,新增了一个Spitter实例到模型中。模型的key是根据对象类型推断得到的,也就是spitter。
再回到表单,我们不仅修改了sf那么简单,我们还将<input>
修改为<sf:input>
,并且将type属性修改为text,并且path属性替代了value,path属性表示模型对象中属性的值。
而在password处,我们使用了<sf:password>
,为的是不用明文显示。
你可能会问,这样做了有啥好处?
因为我们在代码里,有@Valid注解,如果不符合规范,又会调回原处,但是跳回原处后,你会惊奇,我输入的内容一个不剩了!那就GG了。
我们这里用Model保存,并且用flash属性保存了重定向的值。
最后提交错误的结果就是这样的:
你会发现,我们的值还在,不过可能你会疑惑,我到底哪里错了?
那么现在我们写入错误提示:使用<sf:errors>
<sf:form method="POST" commandName="spitter">
First Name: <sf:input path="firstName" /><sf:errors path="firstName"/><br/>
Last Name: <sf:input path="lastName" /><sf:errors path="lastName"/><br/>
Email: <sf:input path="email" /><sf:errors path="email"/><br/>
Username: <sf:input path="username" /><sf:errors path="username"/><br/>
Password: <sf:password path="password" /><sf:errors path="password"/><br/>
<input type="submit" value="Register" />
</sf:form>
当然,这个字显得有些丑,我们可以添加cssClass属性来设定css样式:
<style type="text/css">
span.errors{
color:red;
}
</style>
<sf:form method="POST" commandName="spitter">
First Name: <sf:input path="firstName" /><sf:errors path="firstName"/><br/>
Last Name: <sf:input path="lastName" /><sf:errors path="lastName"/><br/>
Email: <sf:input path="email" /><sf:errors path="email"/><br/>
Username: <sf:input path="username" /><sf:errors path="username" cssClass="errors"/><br/>
Password: <sf:password path="password" /><sf:errors path="password"/><br/>
<input type="submit" value="Register" />
</sf:form>
最后的样式:
然而,这样可能还是不太好,我们希望框也变红色,并且能自定义显示的错误的文字,而不是“个数必须在5和16之前”
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" session="false"%>
<html>
<head>
<title>Spitter</title>
<style type="text/css">
input.errors{
background-color: #ffcccc ;
border: 2px solid red;
color: darkred;
}
span.errors{
color: red;
}
</style>
</head>
<body>
<h1>Register</h1>
<sf:form method="POST" commandName="spitter">
First Name: <sf:input path="firstName" cssErrorClass="errors"/><sf:errors path="firstName" cssClass="errors"/><br/>
Last Name: <sf:input path="lastName" cssErrorClass="errors"/><sf:errors path="lastName" cssClass="errors"/><br/>
Email: <sf:input path="email" cssErrorClass="errors"/><sf:errors path="email" cssClass="errors"/><br/>
Username: <sf:input path="username" cssErrorClass="errors"/><sf:errors path="username" cssClass="errors"/><br/>
Password: <sf:password path="password" cssErrorClass="errors"/><sf:errors path="password" cssClass="errors"/><br/>
<input type="submit" value="Register" />
</sf:form>
</body>
</html>
如果我们要定义message:
package spittr.bean;
public class Spitter {
private Long id;
@NotNull
@Size(min=5, max=16,message = "wrong,please enter the name between 5-16 size")
private String username;
@NotNull
@Size(min=5, max=25,message = "${wrong_password}")
private String password;
@NotNull
@Size(min=2, max=30,message = "${wrong_name}")
private String firstName;
@NotNull
@Size(min=2, max=30,message = "${wrong_name}")
private String lastName;
@NotNull
@Email(message = "not a email")
private String email;
...
}
最后的效果:
Spring的通用标签库
除了表单绑定标签库之外,Spring还提供了更为通用的JSP标签库。
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
你可以把前缀prefix设置为任意值,不过我们这里还是写为s,更为约定俗成。
这里我们不会介绍每个标签,而是介绍几个比较有用的。
到现在为止,我们的jsp还是包含了很多硬编码信息,比如<h1>Bounjour Mondo</h1>
。我们希望完成国际化,我们可以使用<s:message>
:
<h1><s:message code="spittr.welcome"></h1>
按照这里的方式,这个标签会根据Key为spittr.welcom的信息源来渲染文本。
我们需要配置这样的信息源:
暂略,实在用不上。
<s:url>
是一个很小的标签,它是<c:url>
的替代者:
<a href="<s:url href="/spitter/register"/>">Register</a>
这样如果应用的Servlet上下文名为spittr,那么在相应中将会渲染如下的html:
<a href="spittr/spitter/register">Register</a>
当然它还可以变为模版值:
<s:url href="spitter/register" var="regURL"/>
<a href="${regURL}">Register</a>
还有,更重要的功能:
<s:url href="spitter/register" var="regURL">
<s:param name="max" value="60">
</s:url>
或者
<s:url href="spitter/{username}"
var="findUser">
<s:param name="username" value="jbauer">
</s:url>
这样的url分别是:
spitter/register?max=60以及spitter/jbauer
使用Thymeleaf
JSP的副作用是他和HTML太相似了,在视觉上不容易区分。而Thymeleaf模版是原生的,不依赖于标签库,它能够在接受原始HTML的地方进行编辑和渲染。因为它没有与Servlet规范耦合。因此Thtmeleaf模版能够进入JSP所无法涉及的领域。
配置Thymeleaf视图解析器
为了要在Spring中使用Thymeleaf,我们需要配置三个启动Thymeleaf与Spring集成的bean。
- ThemeleafViewResolver:将逻辑视图名解析为Thymeleaf模版视图。
- SpringTemplateEngine:处理模版并渲染结果。
- TemplateResolver:加载Thymeleaf模版。
@Bean
public ITemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setTemplateMode("HTML5");
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("utf-8");
templateResolver.setOrder(1);
templateResolver.setCacheable(false);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
return templateEngine;
}
@Bean
// public ViewResolver viewResolver() {
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setCharacterEncoding("utf-8");
return viewResolver;
}
定义Thymeleaf模版
Thymeleaf很大程度上就是HTML文件。它之所以有自己的作用,是通过自定义的命名空间,为标注的HTML标签集合添加Thymeleaf属性。如下程序清单展现了home.html。
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
th:href="@{/resources/style.css}"></link>
</head>
<body>
<div id="header" th:include="page :: header"></div>
<div id="content">
<h1>Welcome to Spitter</h1>
<a th:href="@{/spittles}">Spittles</a> |
<a th:href="@{/spitter/register}">Register</a>
<br/>
View: <span th:text="${view}">unknown</span>
</div>
<div id="footer" th:include="page :: copy"></div>
</body>
</html>
通过<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
来声明命名空间。
th:href属性,与原生HTML的href属性类似,用来计算动态的url。@{ }
表达式中的计算值就是url的路径。
这样就不用使用<s:url>
或者<a href="xxx.jsp">
的奇怪用法了。
借助Thymeleaf实现表单绑定
为了和之前的表单对比,我们看看registrationForm.jsp:
<sf:lable path="firstName" cssErrorClass="error">FirstName</sf:lable>
<sf:input path="firstName" cssErrorClass="error"/><br/>
现在引入thymeleaf:
<lable th:class="${#fields.hasErrors('firstName')?'error'}">FirstName</lable>
<input type="text" th:field="*{firstName}"
th:class="${#fields.hasErrors{'firstName'}}?'error'"/><br/>
这里我们不再使用Spring JSP标签中的cssClassName属性,而是在标准的HTML标签上使用th:class属性。它会根据给定值决定,检查firstName域有没有错误,如果没有错误,将不会渲染class属性。
th:field属性用来引用后端对象的firstName域。
以下是完整的表单页面:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet" type="text/css"
th:href="@{/resources/style.css}"></link>
</head>
<body>
<div id="header" th:include="page :: header"></div>
<div id="content">
<h1>Register</h1>
<form method="POST" th:object="${spitter}">
<div class="errors" th:if="${#fields.hasErrors('*')}">
<ul>
<li th:each="err : ${#fields.errors('*')}"
th:text="${err}">Input is incorrect</li>
</ul>
</div>
<label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>:
<input type="text" th:field="*{firstName}"
th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('lastName')}? 'error'">Last Name</label>:
<input type="text" th:field="*{lastName}"
th:class="${#fields.hasErrors('lastName')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('email')}? 'error'">Email</label>:
<input type="text" th:field="*{email}"
th:class="${#fields.hasErrors('email')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('username')}? 'error'">Username</label>:
<input type="text" th:field="*{username}"
th:class="${#fields.hasErrors('username')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('password')}? 'error'">Password</label>:
<input type="password" th:field="*{password}"
th:class="${#fields.hasErrors('password')}? 'error'" /><br/>
<input type="submit" value="Register" />
</form>
</div>
<div id="footer" th:include="page :: copy"></div>
</body>
</html>
你可能会想知道“${}”和"*{}"有什么区别,前一个是变量表达式,而后一个是选择式