Spring MVC入门案例(2)

本文详细介绍了基于 Spring MVC 架构实现登录界面的构建、请求处理流程、业务逻辑处理及视图展示的全过程。从登录界面的 HTML 表单开始,到 Web 服务器如何解析请求并将其分发给 Dispatcher Servlet,再到 Dispatcher Servlet 如何调用具体业务逻辑单元(如 LoginAction)处理请求数据,最后到视图解析器如何生成并返回最终的页面展示。文章还涉及了参数绑定、异常处理、视图渲染等方面的知识,旨在为开发者提供一个全面的 Spring MVC 登录功能实现指南。

首先来看登录界面

对应的index.html:

[html]  view plain copy
  1. <html>  
  2.   
  3. <body>  
  4.   
  5. <form method="POST" action="login.do">  
  6.   
  7. <p align="center">登录</p>  
  8.   
  9. <br>  
  10.   
  11. 用户名:  
  12.   
  13. <input type="text" name="username" >  
  14.   
  15. <br>  
  16.   
  17. 密 码 :  
  18.   
  19. <input type="password" name="password" >  
  20.   
  21. <br>  
  22.   
  23. <p>  
  24.   
  25. <input type="submit" value="提交" name="B1">  
  26.   
  27. <input type="reset" value="重置" name="B2">  
  28.   
  29. </p>  
  30.   
  31. </form>  
  32.   
  33. </body>  
  34.   
  35. </html>  



很简单的一个登录界面,其中包含了一个用以输入用户名密码的form,针对此form的提

交将被发送到"login.do"

MVC 关键流程的第一步,即收集页面输入参数,并转换为请求数据对象。这个静态页面提

供了一个基本的输入界面,下面这些输入的数据将被发送至何处,将如何被转换为请求数据对

象?

现在来看接下来发发生的事情

当用户输入用户名密码提交之后,此请求被递交给Web 服务器处理,上面我们设定form

提交目标为"login.do",那么Web服务器将如何处理这个请求?

显然,标准Http 协议中,并没有以.do 为后缀的服务资源,这是我们自己定义的一种请

求匹配模式。此模式在web.xml中设定:

[html]  view plain copy
  1. <?xml version="1.0" encoding="ISO-8859-1"?>  
  2.   
  3. <web-app xmlns="http://java.sun.com/xml/ns/j2ee"  
  4.   
  5. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  6.   
  7. xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee  
  8.   
  9. http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"  
  10.   
  11. version="2.4">  
  12.   
  13. <servlet> ⑴  
  14.   
  15. <servlet-name>Dispatcher</servlet-name>  
  16.   
  17. <servlet-class>  
  18.   
  19. org.springframework.web.servlet.DispatcherServlet  
  20.   
  21. </servlet-class>  
  22.   
  23. <init-param>  
  24.   
  25. <param-name>contextConfigLocation</param-name>  
  26.   
  27. <param-value>/WEB-INF/Config.xml</param-value>  
  28.   
  29. </init-param>  
  30.   
  31. </servlet>  
  32.   
  33. <servlet-mapping> ⑵  
  34.   
  35. <servlet-name>Dispatcher</servlet-name>  
  36.   
  37. <url-pattern>*.do</url-pattern>  
  38.   
  39. </servlet-mapping>  
  40.   
  41. </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,此文件包含了所有的“请求/处理单元”关系映射设定,以及返回

时表现层的一些属性设置。

[html]  view plain copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2.   
  3. <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"  
  4.   
  5. "http://www.springframework.org/dtd/spring-beans.dtd">  
  6.   
  7. <beans>  
  8.   
  9. <!--Definition of View Resolver -->  
  10.   
  11. <bean id="viewResolver" ⑴  
  12.   
  13. class="org.springframework.web.servlet.view.InternalResou  
  14.   
  15. rceViewResolver">  
  16.   
  17. <property name="viewClass"> ⑵  
  18.   
  19. <value>  
  20.   
  21. org.springframework.web.servlet.view.JstlView  
  22.   
  23. </value>  
  24.   
  25. </property>  
  26.   
  27. <property name="prefix"> ⑶  
  28.   
  29. <value>  
  30.   
  31. /WEB-INF/view/  
  32.   
  33. </value>  
  34.   
  35. </property>  
  36.   
  37. <property name="suffix"> ⑷  
  38.   
  39. <value>.jsp</value>  
  40.   
  41. </property>  
  42.   
  43. </bean>  
  44.   
  45. <!--Request Mapping -->  
  46.   
  47. <bean id="urlMapping" ⑸  
  48.   
  49. class="org.springframework.web.servlet.handler.SimpleUr  
  50.   
  51. lHandlerMapping">  
  52.   
  53. <property name="mappings">  
  54.   
  55. <props>  
  56.   
  57. <prop key="/login.do">LoginAction</prop>  
  58.   
  59. </props>  
  60.   
  61. </property>  
  62.   
  63. </bean>  
  64.   
  65. <!---Action Definition-->  
  66.   
  67. <bean id="LoginAction" ⑹  
  68.   
  69. class="net.xiaxin.action.LoginAction">  
  70.   
  71. <property name="commandClass"> ⑺  
  72.   
  73. <value>net.xiaxin.action.LoginInfo</value>  
  74.   
  75. </property>  
  76.   
  77. <property name="fail_view"> ⑻  
  78.   
  79. <value>loginfail</value>  
  80.   
  81. </property>  
  82.   
  83. <property name="success_view">  
  84.   
  85. <value>main</value>  
  86.   
  87. </property>  
  88.   
  89. </bean>  
  90.   
  91. </beans>  


⑴ Resolver设定

Resolver将把输出结果与输出界面相融合,为表现层提供呈现资源。

⑵ View ResolverviewClass参数

这里我们使用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 Resolverprefixsuffix参数

指定了表现层资源的前缀和后缀,运行时,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,它封装了登录请求所需的数据内容:

[java]  view plain copy
  1. public class LoginInfo {  
  2.   
  3. private String username;  
  4.   
  5. private String password;  
  6.   
  7. public String getPassword() {  
  8.   
  9. return password;  
  10.   
  11. }  
  12.   
  13. public void setPassword(String password) {  
  14.   
  15. this.password = password;  
  16.   
  17. }  
  18.   
  19. public String getUsername() {  
  20.   
  21. return username;  
  22.   
  23. }  
  24.   
  25. public void setUsername(String username) {  
  26.   
  27. this.username = username;  
  28.   
  29. }  
  30.   
  31. }  


Spring会根据LoginActioncommandClass定义自动加载对应的LoginInfo

实例。

之后,对Http 请求中的参数进行遍历,并查找LoginInfo 对象中是否存在与之同

名的属性,如果找到,则将此参数值复制到LoginInfo对象的同名属性中.

请求数据转换完成之后,我们得到了一个封装了所有请求参数的Java 对象,并将此

对象作为输入参数传递给LoginAction

⑻ 返回视图定义

对于这里的LoginAction 而言,有两种返回结果,即登录失败时返回错误界面,登

录成功时进入系统主界面。

对应我们配置了fail_viewsuccess_view两个自定义参数。

参数值将由Resolver进行处理,为其加上前缀后缀,如对于fail_view而言,实

际的视图路径为/WEB-INF/view/loginfail.jsp

之后,Resolver 会将LoginAction的返回数据与视图相融合,返回最终的显示界

面。

业务逻辑处理单元:

[java]  view plain copy
  1. LoginAction.java:  
  2.   
  3. public class LoginAction extends SimpleFormController {  
  4.   
  5. private String fail_view;  
  6.   
  7. private String success_view;  
  8.   
  9. protected ModelAndView onSubmit( ⑴  
  10.   
  11. Object cmd,  
  12.   
  13. BindException ex  
  14.   
  15. )throws Exception {  
  16.   
  17. LoginInfo loginInfo = (LoginInfo) cmd; ⑵  
  18.   
  19. HashMap result_map = new HashMap();  
  20.   
  21. if (login(loginInfo) == 0) {  
  22.   
  23. result_map.put("logininfo", loginInfo);  
  24.   
  25. List msgList = new LinkedList();  
  26.   
  27. msgList.add("msg1");  
  28.   
  29. msgList.add("msg2");  
  30.   
  31. msgList.add("msg3");  
  32.   
  33. result_map.put("messages", msgList);  
  34.   
  35. return new  
  36.   
  37. ModelAndView(this.getSuccess_view(), result_map); ⑶  
  38.   
  39. else {  
  40.   
  41. result_map.put("failmsg"new String("Sorry, you input the wrong username or password!"));  
  42.   
  43. return new ModelAndView(this.getFail_view(), result_map);  
  44.   
  45. }  
  46.   
  47. }  
  48.   
  49. private int login(LoginInfo loginInfo) {  
  50.   
  51. if ("Erica".equalsIgnoreCase(loginInfo.getUsername())  
  52.   
  53. &&"mypass".equals(loginInfo.getPassword())) {  
  54.   
  55. return 0;  
  56.   
  57. }  
  58.   
  59. return 1;  
  60.   
  61. }  
  62.   
  63. public String getFail_view() {  
  64.   
  65. return fail_view;  
  66.   
  67. }  
  68.   
  69. public String getSuccess_view() {  
  70.   
  71. return success_view;  
  72.   
  73. }  
  74.   
  75. public void setFail_view(String string) {  
  76.   
  77. fail_view = string;  
  78.   
  79. }  
  80.   
  81. public void setSuccess_view(String string) {  
  82.   
  83. success_view = string;  
  84.   
  85. }  
  86.   
  87. }  


其中:

⑴ onSubmit方法

我们在子类中覆盖了父类的onSubmit方法;而onSubmit方法用于处理业务请求。

负责数据封装和请求分发的Dispatcher,将对传入的HttpServletRequest进行

封装,形成请求数据对象,之后根据配置文件,调用对应业务逻辑类的入口方法(这

里就是LoginAction)的onSubmit()方法,并将请求数据对象及其他相关资源引

用传入。

protected ModelAndView onSubmit(

Object cmd,

BindException ex

)

onSubmit方法包含了两个参数:Object cmdBindException ex

前面曾经多次提到请求数据对象,这个名为cmdObject型参数,正是传入的请求

数据对象的引用。

BindException ex参数则提供了数据绑定错误的跟踪机制。它作为错误描述工具,

用于向上层反馈错误信息。

Spring MVC中,BindException将被向上层表现层反馈,以便在表现层统一处

理异常情况(如显示对应的错误提示),这一机制稍后在“输入参数合法性校验”部

分再具体探讨。

onSubmit还有另外一个签名版本:

[java]  view plain copy
  1. protected ModelAndView onSubmit(  
  2.   
  3. HttpServletRequest request,  
  4.   
  5. HttpServletResponse response,  
  6.   
  7. Object cmd,  
  8.   
  9. BindException ex  
  10.   
  11. )  


可以看到,类似ServletdoGet/doPost方法,此版本的onSubmit方法签名中

包含了Servlet规范中的HttpServletRequestHttpServletResponse以提

供与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:

界面显示效果如下:

[java]  view plain copy
  1. <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>  
  2.   
  3. <html>  
  4.   
  5. <body>  
  6.   
  7. <p>Login Success!!!</p>  
  8.   
  9. <p>Current User:  
  10.   
  11. <c:out value="${logininfo.username}"/><br>  
  12.   
  13. </p>  
  14.   
  15. <p>Your current messages:</p>  
  16.   
  17. <c:forEach items="${messages}"  
  18.   
  19. var="item"  
  20.   
  21. begin="0"  
  22.   
  23. end="9"  
  24.   
  25. step="1"  
  26.   
  27. varStatus="var">  
  28.   
  29. <c:if test="${var.index % 2 == 0}">  
  30.   
  31. *  
  32.   
  33. </c:if>  
  34.   
  35. ${item}<br>  
  36.   
  37. </c:forEach>  
  38.   
  39. </body>  
  40.   
  41. </html>  



登录失败后的页面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 进行循环和

逻辑判断的样例)。

实际上这只是个普通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 LanguageEL)的语法。它定义了一个表达式,

其中的表达式可以是一个常量(如上),也可以是一个具体的表达语句(如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,如果没有找到则依次到RequestSession

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}

Ø ${12}

结果为表达式计算结果,即整数值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,循环范围是从09,每次步长为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 序列图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值