Spring MVC学习指南
rel="File-List" href="file:///C:%5CDOCUME%7E1%5Csyn%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
以下这个学习案例是我最近学习Spring MVC时跟从一本书上的示例,原文中的示例代码有一些小错误,不过我在调试的过程中已经给予了修正,如还有其它错误,还请各位批评指正。
对于现有较成熟的Model-View-Control(MVC)框架而言,其解决的主要问题无外乎下
面几部分:
1. 将Web页面中的输入元素封装为一个(请求)数据对象。
2. 根据请求的不同,调度相应的逻辑处理单元,并将(请求)数据对象作为参数传入。
3. 逻辑处理单元完成运算后,返回一个结果数据对象。
4. 将结果数据对象中的数据与预先设计的表现层相融合并展现给用户。
各个MVC 实现固然存在差异,但其中的关键流程大致如上。结合一个实例,我们来看看这
几个关键流程在Spring MVC框架中的处理手法。
下面的实例,实现了一个常见的用户登录逻辑,即用户通过用户名和密码登录,系统对用
户名和密码进行检测,如果正确,则在页面上显示几条通知信息。如果登录失败,则返回失败
界面。
(示例中,表示层以JSP2.0实现。)
出于简洁考虑,这里的“用户名/密码”检测以及通知信息的生成均在代码中以硬编码实现。
首先来看登录界面:
对应的index.html:
<html>
<body>
<form method="POST" action="login.do">
<p align="center">登录</p>
<br>
用户名:
<input type="text" name="username" >
<br>
密 码 :
<input type="password" name="password" >
<br>
<p>
<input type="submit" value="提交" name="B1">
<input type="reset" value="重置" name="B2">
</p>
</form>
</body>
</html>
很简单的一个登录界面,其中包含了一个用以输入用户名密码的form,针对此form的提
交将被发送到"login.do"
MVC 关键流程的第一步,即收集页面输入参数,并转换为请求数据对象。这个静态页面提
供了一个基本的输入界面,下面这些输入的数据将被发送至何处,将如何被转换为请求数据对
象?
现在来看接下来发发生的事情:
当用户输入用户名密码提交之后,此请求被递交给Web 服务器处理,上面我们设定form
提交目标为"login.do",那么Web服务器将如何处理这个请求?
显然,标准Http 协议中,并没有以.do 为后缀的服务资源,这是我们自己定义的一种请
求匹配模式。此模式在web.xml中设定:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<servlet> ⑴
<servlet-name>Dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/Config.xml</param-value>
</init-param>
</servlet>
<servlet-mapping> ⑵
<servlet-name>Dispatcher</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
⑴ Servlet定义
这里我们定义了请求分发Servlet,即:
org.springframework.web.servlet.DispatcherServlet
DispatcherServlet 是Spring MVC 中负责请_____求调度的核心引擎,所有的请求将
由此Servlet 根据配置分发至各个逻辑处理单元。其内部同时也维护了一个
ApplicationContext实例。
我们在<init-param>节点中配置了名为“contextConfigLocation”的
Servlet参数,此参数指定了Spring配置文件的位置“/WEB-INF/Config.xml”。
如果忽略此设定,则默认为“/WEB-INF/<servlet name>-servlet.xml”,其
中<servlet name>以Servlet 名替换(在当前环境下,默认值也就是
“/WEB-INF/Dispatcher-servlet.xml)。
⑵ 请求映射
我们将所有以.do结尾的请求交给Spring MVC进行处理。当然,也可以设为其他值,
如.action、.action等。
通过以上设定,Web 服务器将把登录界面提交的请求转交给Dispatcher 处理,
Dispatcher将提取请求(HttpServletRequest)中的输入数据,分发给对应的处理单元,
各单元处理完毕后,将输出页面返回给Web服务器,再由Web服务器返回给用户浏览器。
Dispatcher 根据什么分发这些请求?显然,我们还需要一个配置文件加以设定。这也就
是上面提及的Config.xml,此文件包含了所有的“请求/处理单元”关系映射设定,以及返回
时表现层的一些属性设置。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<!--Definition of View Resolver -->
<bean id="viewResolver" ⑴
class="org.springframework.web.servlet.view.InternalResou
rceViewResolver">
<property name="viewClass"> ⑵
<value>
org.springframework.web.servlet.view.JstlView
</value>
</property>
<property name="prefix"> ⑶
<value>
/WEB-INF/view/
</value>
</property>
<property name="suffix"> ⑷
<value>.jsp</value>
</property>
</bean>
<!--Request Mapping -->
<bean id="urlMapping" ⑸
class="org.springframework.web.servlet.handler.SimpleUr
lHandlerMapping">
<property name="mappings">
<props>
<prop key="/login.do">LoginAction</prop>
</props>
</property>
</bean>
<!---Action Definition-->
<bean id="LoginAction" ⑹
class="net.xiaxin.action.LoginAction">
<property name="commandClass"> ⑺
<value>net.xiaxin.action.LoginInfo</value>
</property>
<property name="fail_view"> ⑻
<value>loginfail</value>
</property>
<property name="success_view">
<value>main</value>
</property>
</bean>
</beans>
⑴ Resolver设定
Resolver将把输出结果与输出界面相融合,为表现层提供呈现资源。
⑵ View Resolver的viewClass参数
这里我们使用JSP页面作为输出,因此,设定为:
org.springframework.web.servlet.view.JstlView
其余可选的viewClass还有:
Ø org.springframework.web.servlet.view.freemarker.FreeMarker
View(用于基于FreeMarker模板的表现层实现)
Ø org.springframework.web.servlet.view.velocity.VelocityView
(用于基于velocity模板的表现层实现)
等。
⑶⑷ View Resolver的prefix和suffix参数
指定了表现层资源的前缀和后缀,运行时,Spring 将为指定的表现层资源自动追加
前缀和后缀,以形成一个完整的资源路径。另参见⑻
⑸ “请求/处理单元”关系映射
可以看到,这里我们将“/login.do”请求映射到处理单元LoginAction。
<props>节点下可以有多个映射关系存在,目前我们只定义了一个。
⑹ LoginAction定义
这里定义了逻辑处理单元LoginAction 的具体实现,这里,LoginAction 的实现
类为net.xiaxin.action.LoginAction。
⑺ LoginAction的请求数据对象
commandClass 参数源于LoginAction 的基类BaseCommandController,
BaseCommandControlle 包含了请求数据封装和验证方法
( BaseCommandController.bindAndValidate ) , 它将根据传入的
HttpServletRequest构造请求数据对象。
这里我们指定commandClass 为net.xiaxin.action.LoginInfo,这是一个非
常简单的Java Bean,它封装了登录请求所需的数据内容:
public class LoginInfo {
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
Spring会根据LoginAction的commandClass定义自动加载对应的LoginInfo
实例。
之后,对Http 请求中的参数进行遍历,并查找LoginInfo 对象中是否存在与之同
名的属性,如果找到,则将此参数值复制到LoginInfo对象的同名属性中.
请求数据转换完成之后,我们得到了一个封装了所有请求参数的Java 对象,并将此
对象作为输入参数传递给LoginAction。
⑻ 返回视图定义
对于这里的LoginAction 而言,有两种返回结果,即登录失败时返回错误界面,登
录成功时进入系统主界面。
对应我们配置了fail_view、success_view两个自定义参数。
参数值将由Resolver进行处理,为其加上前缀后缀,如对于fail_view而言,实
际的视图路径为/WEB-INF/view/loginfail.jsp。
之后,Resolver 会将LoginAction的返回数据与视图相融合,返回最终的显示界
面。
业务逻辑处理单元:
LoginAction.java:
public class LoginAction extends SimpleFormController {
private String fail_view;
private String success_view;
protected ModelAndView onSubmit( ⑴
Object cmd,
BindException ex
)throws Exception {
LoginInfo loginInfo = (LoginInfo) cmd; ⑵
rel="File-List" href="file:///C:%5CDOCUME%7E1%5Csyn%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">HashMap result_map = new HashMap();
if (login(loginInfo) == 0) {
result_map.put("logininfo", loginInfo);
List msgList = new LinkedList();
msgList.add("msg1");
msgList.add("msg2");
msgList.add("msg3");
result_map.put("messages", msgList);
return new
ModelAndView(this.getSuccess_view(), result_map); ⑶
} else {
result_map.put("failmsg", new String("Sorry, you input the wrong username or password!"));
return new ModelAndView(this.getFail_view(), result_map);
}
}
private int login(LoginInfo loginInfo) {
if ("Erica".equalsIgnoreCase(loginInfo.getUsername())
&& "mypass".equals(loginInfo.getPassword())) {
return 0;
}
return 1;
}
public String getFail_view() {
return fail_view;
}
public String getSuccess_view() {
return success_view;
}
public void setFail_view(String string) {
fail_view = string;
}
public void setSuccess_view(String string) {
success_view = string;
}
}
其中:
⑴ onSubmit方法
我们在子类中覆盖了父类的onSubmit方法;而onSubmit方法用于处理业务请求。
负责数据封装和请求分发的Dispatcher,将对传入的HttpServletRequest进行
封装,形成请求数据对象,之后根据配置文件,调用对应业务逻辑类的入口方法(这
里就是LoginAction)的onSubmit()方法,并将请求数据对象及其他相关资源引
用传入。
protected ModelAndView onSubmit(
Object cmd,
BindException ex
)
onSubmit方法包含了两个参数:Object cmd和BindException ex。
前面曾经多次提到请求数据对象,这个名为cmd的Object型参数,正是传入的请求
数据对象的引用。
BindException ex参数则提供了数据绑定错误的跟踪机制。它作为错误描述工具,
用于向上层反馈错误信息。
在Spring MVC中,BindException将被向上层表现层反馈,以便在表现层统一处
理异常情况(如显示对应的错误提示),这一机制稍后在“输入参数合法性校验”部
分再具体探讨。
onSubmit还有另外一个签名版本:
protected ModelAndView onSubmit(
HttpServletRequest request,
HttpServletResponse response,
Object cmd,
BindException ex
)
可以看到,类似Servlet的doGet/doPost方法,此版本的onSubmit方法签名中
包含了Servlet规范中的HttpServletRequest、HttpServletResponse以提
供与Web服务器的交互功能(如Session的访问)。此参数类型的onSubmit方法
的调用优先级较高。也就是说,如果我们在子类中同时覆盖了这两个不同参数的
onSubmit方法,那么只有此版本的方法被执行,另一个将被忽略。
⑵ 将输入的请求数据对象强制转型为预定义的请求对象类型。
⑶ 返回处理结果
ModelAndView类包含了逻辑单元返回的结果数据集和表现层信息。ModelAndView
本身起到关系保存的作用。它将被传递给Dispatcher,由Dispatcher 根据其中
保存的结果数据集和表现层设定合成最后的界面。
这里我们用到了两种签名版本的ModelAndView构造方法:
Ø public ModelAndView(String viewname)
返回界面无需通过结果数据集进行填充。
Ø public ModelAndView(String viewname, Map model)
返回界面由指定的结果数据集加以填充。可以看到,结果数据集采用了Map接口
实现的数据类型。其中包含了返回结果中的各个数据单元。关于结果数据集在界
面中的填充操作,可参见下面关于返回界面的描述。
上面这两个版本的构造子中,通过viewname指定了表示层资源。
另外,我们也可以通过传递View对象指定表示层资源。
Ø public ModelAndView(View view)
Ø public ModelAndView(View view, Map model)
我们可以结合RedirectView完成转向功能,如:
return new ModelAndView(
new RedirectView(“/redirected.jsp”
));
当然,我们也可以在带有HttpServletRequest参数的onSubmit方法实现中,通
过HttpServletRequest/HttpServletResponse完成forward/redirect功
能,这两种途径可以达到同样的效果。
最后,来看返回界面:
错误返回界面loginfail.jsp只是个纯html文件(为了与View Resolver中设
定的后缀相匹配,因此以.jsp作为文件后缀),这里就不再浪费篇幅。
再看成功登录后的页面main.jsp:
界面显示效果如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<html>
<body>
<p>Login Success!!!</p>
<p>Current User:
<c:out value="${logininfo.username}"/><br>
</p>
<p>Your current messages:</p>
<c:forEach items="${messages}"
var="item"
begin="0"
end="9"
step="1"
varStatus="var">
<c:if test="${var.index % 2 == 0}">
*
</c:if>
${item}<br>
</c:forEach>
</body>
</html>
rel="File-List" href="file:///C:%5CDOCUME%7E1%5Csyn%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
登录失败后的页面loginfail.jsp:
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt"%>
<html>
<body>
<p>Login Fail!!!</p><c:out value="${failmsg}" />
</body>
</ html >页面逻辑非常简单,首先显示当前登录用户的用户名。然后循环显示当前用户的通知消息
“messages”。如果当前循环索引为奇数,则在消息前追加一个“*”号(这个小特性在这里
似乎有些多余,但却为不熟悉JSTL 的读者提供了如何使用JSTL Core taglib 进行循环和
逻辑判断的样例)。
rel="File-List" href="file:///C:%5CDOCUME%7E1%5Csyn%5CLOCALS%7E1%5CTemp%5Cmsohtml1%5C01%5Cclip_filelist.xml">
实际上这只是个普通JSTL/JSP页面,并没有任何特殊之处,如果说有些值得研究的技术,
也就是其中引用的JSTL Core Taglib
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
上面这句话申明了页面中所引用的taglib,指定其前缀为“c”,也就是说,在页面中,
所有以“c”为前缀,形同<c:xxxx>的节点都表明是此taglib的引用,在这里,也就是对JSTL
Core Lib的引用。
这里需要注意的是,笔者所采用的Web 容器为Tomcat 5(支持Servlet 2.4/JSP2.0
规范)以及Apache JSTL 2.0(http://jakarta.apache.org/taglibs/index.html)。
<c:out value="${logininfo.username}"/>
<c:out>将value 中的内容输出到当前位置,这里也就是把logininfo 对象的
username属性值输出到页面当前位置。
${……}是JSP2.0 中的Expression Language(EL)的语法。它定义了一个表达式,
其中的表达式可以是一个常量(如上),也可以是一个具体的表达语句(如forEach循环体中
的情况)。典型案例如下:
Ø ${logininfo.username}
这表明引用logininfo 对象的username 属性。我们可以通过“.”操作符引
用对象的属性,也可以用“[]”引用对象属性,如${logininfo[username]}
与${logininfo.username}达到了同样的效果。
“[]”引用方式的意义在于,如果属性名中出现了特殊字符,如“.”或者“-”,
此时就必须使用“[]”获取属性值以避免语法上的冲突(系统开发时应尽量避免
这一现象的出现)。
与之等同的JSP Script大致如下:
LoginInfo logininfo =
(LoginInfo)session.getAttribute(“logininfo”);
String username = logininfo.getUsername();
可以看到,EL大大节省了编码量。
这里引出的另外一个问题就是,EL 将从哪里找到logininfo 对象,对于
${logininfo.username}这样的表达式而言,首先会从当前页面中寻找之前是
否定义了变量logininfo,如果没有找到则依次到Request、Session、
Application 范围内寻找,直到找到为止。如果直到最后依然没有找到匹配的
变量,则返回null.
如果我们需要指定变量的寻找范围,可以在EL表达式中指定搜寻范围:
${pageScope.logininfo.username}
${requestScope.logininfo.username}
${sessionScope.logininfo.username}
${applicationScope.logininfo.username}
在Spring 中,所有逻辑处理单元返回的结果数据,都将作为Attribute 被放
置到HttpServletRequest 对象中返回(具体实现可参见Spring 源码中
org.springframework.web.servlet.view.InternalResourceView.
exposeModelAsRequestAttributes方法的实现代码),也就是说Spring
MVC 中,结果数据对象默认都是requestScope。因此,在Spring MVC 中,
以下寻址方法应慎用:
${sessionScope.logininfo.username}
${applicationScope.logininfo.username}
Ø ${1+2}
结果为表达式计算结果,即整数值3。
Ø ${i>1}
如果变量值i>1的话,将返回bool类型true。与上例比较,可以_____发现EL会自
动根据表达式计算结果返回不同的数据类型。
表达式的写法与java代码中的表达式编写方式大致相同。
<c:forEach items="${messages}"
var="item"
begin="0"
end="9"
step="1"
varStatus="var">
……
</c:forEach>
上面这段代码的意思是针对messages 对象进行循环,循环中的每个循环项的引用变量为
item,循环范围是从0到9,每次步长为1。而varStatus则定义了一个循环状态变量var,
循环状态变量中保存了循环进行时的状态信息,包括以下几个属性:
属性 类型 说明
index int 当前循环索引号
count int 成员总数
first boolean 当前成员是否首位成员
last boolean 当前成员是否末尾成员
再看:
<c:if test="${var.index % 2 == 0}">
*
</c:if>
这段代码演示了判定Tag <c:if>的使用方法。可以看到,其test属性指明了判定条件,
判定条件一般为一个EL表达式。
<c:if>并没有提供else子句,使用的时候可能有些不便,此时我们可以通过<c:choose>
tag来达到类似的目的:
<c:choose>
<c:when test="${var.index % 2 == 0}">
*
</c:when>
<c:otherwise>
!
</c:otherwise>
</c:choose>
类似Java 中的switch 语句,<c:choose>提供了复杂判定条件下的简化处理手法。其
中 <c:when>子句类似case子句,可以出现多次。上面的代码,在奇数行时输出“*”号,
而偶数行时输出“!”。
通过<c:choose>改造后的输出页面:
至此,一个典型的请求 / 响应过程结束。通过这个过程,我们也了解了 Spring MVC 的核心实现机制。对其进行总结,得到以下 UML 序列图:![](https://p-blog.csdn.net/images/p_blog_csdn_net/yinianshen/EntryImages/20081123/uml.jpg)