Spring MVC @SessionAttributes

作者:範宗雲 来源:原创 发布时间:2015-03-25 归档:springmvc

开发环境 : JDK 7 Maven 3 Tomcat 7 Spring 4.1.5 Eclipse Luna
@SessionAttributes 注解在类级别上, 表明属性的作用域是 session。
下面用例将展示如何通过 @SessionAttributes 注解将一个属性放入 session 域, 以及放入 session 域后如何来删除这个属性。
示例代码片段 1
			@Controller
			@SessionAttributes("randomCode")
			public class LoginController {
			
				private static final String MESSAGE = "message";
				private static final String DEFAULT_USER = "游客";
				
				/**
				 * 登录页面
				 * 
				 * @param model
				 *            ModelMap
				 * @return
				 */
				@RequestMapping(value = "/login", method = GET)
				public String login(ModelMap model) {
					model.addAttribute("randomCode", getRandomCode());
					return "login";
				}
			
				/**
				 * 用户登录
				 * 
				 * @param randomCode
				 *            验证码
				 * @param model
				 *            ModelMap
				 * @param session
				 *            HttpSession
				 * @param sessionStatus
				 *            SessionStatus
				 * @param username
				 *            用户名
				 * @param inputCode
				 *            用户输入的验证码
				 * @return
				 */
				@RequestMapping(value = "/login", method = POST)
				public String login(@ModelAttribute("randomCode") String randomCode,
						ModelMap model, HttpSession session, SessionStatus sessionStatus,
						String username, String inputCode) {
					if (!randomCode.equals(inputCode)) {
						model.addAttribute(MESSAGE, "验证码错误, 请重新登录!");
						return "login";
					}
					if (!StringUtils.hasText(username)) {
						username = DEFAULT_USER;
					}
					session.setAttribute("currUser", username);
					sessionStatus.setComplete();
					return "home";
				}
				
				/**
				 * 生成随机验证码
				 * 
				 * @return
				 */
				private String getRandomCode() {
					StringBuilder randomCode = new StringBuilder();
					for (int i = 0; i < 4; i++) {
						randomCode.append(ThreadLocalRandom.current().nextInt(9));
					}
					return randomCode.toString();
				}
			
			}
			
login.jsp
			<body>
			  <div id="form" align="center">
			    <div id="message">${message}</div>
			    <form method="post">
			      <table>
			        <tr>
			          <td><label for="username">用户名 :</label></td>
			          <td><input type="text" name="username" placeholder="给自己取一个名字吧"></td>
			        </tr>
			        <tr>
			          <td><label for="inputCode">验证码 :</label></td>
			          <td><input type="text" name="inputCode" placeholder="请输入右边的验证码"></td>
			          <td><div id="randomCode">${randomCode}</div></td>
			        </tr>
			        <tr>
			          <td> </td>
			          <td><input type="submit" value="登 录"></td>
			        </tr>
			      </table>
			    </form>
			  </div>
			</body>
			
GET /login
处理方法 login ( GET ) 被执行, 首先是通过 getRandomCode() 产生一个 4 位数的随机数字作为验证码, 并通过 model.addAttribute("randomCode", getRandomCode()) 将产生的验证码放到模型中, 放到模型中的属性默认是 request 域, 即只在本次请求有效。
这里由于放入模型的 key 与 @SessionAttributes("randomCode") 的 key 一致, 因此 randomCode 属性并不放到 request 域, 而放到 session 域。
若需要将多个属性放到 session 域, 可以这样做, @SessionAttributes({"prop1", "prop2", "prop3"})。
用户名不是必须的, 输入验证码, 点登录或回车, 将发起 POST /login 请求。
处理方法 login ( POST ) 执行过程中, @ModelAttribute("randomCode") String randomCode, 是从 session 域中将 randomCode 属性取出。
若验证码正确, 将执行 HttpSession.setAttribute("currUser", username), 来将用户名放入到 HttpSession, 并且执行 SessionStatus.setComplete(), 作用效果如下。
			<body>
			  <h1>欢迎您, ${currUser}!${randomCode}</h1>
			  <h3><a href="${pageContext.request.contextPath}/user/security">用户账户安全中心</a></h3>
			</body>
			
可以看到, ${currUser} 有值, ${randomCode} 没值, 这是因为执行 SessionStatus.setComplete() 后, 通过 @SessionAttributes 放入 session 中的属性清除了, 但不会清除 HttpSession 中的属性。
示例代码片段 2
下面用例将展示 @SessionAttributes 属性未初始化时, 如何防止 500 异常。
			@Controller
			@RequestMapping("/user/security")
			@SessionAttributes("smsCode")
			public class UserAccountSecurity {
				
				@RequestMapping
				public String input() {
					return "input";
				}
			
				/**
				 * 发送短信验证码
				 * 
				 * @param model
				 *            ModelMap
				 * @return
				 */
				@RequestMapping(value = "/sendSmsCode", method = GET)
				public String sendSmsCode(ModelMap model) {
					String smsCode = getRandomCode();
					model.addAttribute("smsCode", smsCode);
					model.addAttribute("message", "验证码已发送, 请查收[" + smsCode + "]");
					return "input";
				}
			
				/**
				 * 验证手机号码
				 * 
				 * @param smsCode
				 *            系统产生的验证码
				 * @param model
				 *            ModelMap
				 * @param sessionStatus
				 *            SessionStatus
				 * @param inputCode
				 *            SessionStatus
				 * @return
				 */
				@RequestMapping(value = "/verifyPhone", method = POST)
				public String verifyPhone(@ModelAttribute("smsCode") String smsCode,
						ModelMap model, SessionStatus sessionStatus, String inputCode) {
					if (!StringUtils.hasText(inputCode)) {
						model.addAttribute("message", "请输入验证码!");
					} else {
						if (inputCode.equals(smsCode)) {
							sessionStatus.setComplete();
							model.addAttribute("message", "绑定手机成功!");
						} else {
							model.addAttribute("message", "验证码错误, 请重新输入!");
						}
					}
					return "input";
				}
			
				/**
				 * 验证手机号码
				 * 
				 * @param model
				 *            ModelMap
				 * @param sessionStatus
				 *            SessionStatus
				 * @param inputCode
				 *            用户输入的验证码
				 * @return
				 */
				@RequestMapping(value = "/verifyMobile", method = POST)
				public String verifyMobile(ModelMap model, SessionStatus sessionStatus, String inputCode) {
					if (model.containsAttribute("smsCode")) {
						String smsCode = (String) model.get("smsCode");
						if (smsCode.equals(inputCode)) {
							sessionStatus.setComplete();
							model.addAttribute("message", "绑定手机成功!");
						} else {
							model.addAttribute("message", "验证码错误, 请重新输入!");
						}
					} else {
						model.addAttribute("message", "请发送验证码到你的手机以完成验证!");
					}
					return "input";
				}
			
				/**
				 * 产生随机数
				 * 
				 * @return
				 */
				private String getRandomCode() {
					StringBuilder randomCode = new StringBuilder();
					for (int i = 0; i < 6; i++) {
						randomCode.append(ThreadLocalRandom.current().nextInt(9));
					}
					return randomCode.toString();
				}
			
			}
			
input.jsp
			<body>
			  <div id="form" align="center">
			    <div id="message">${message}</div>
			    <%-- <form action="${pageContext.request.contextPath}/user/security/verifyMobile" method="post"> --%>
			    <form action="${pageContext.request.contextPath}/user/security/verifyPhone" method="post">
			      <input type="text" name="inputCode" placeholder="输入手机收到的验证码">
			      <span class="smsCode" 
			        onclick="javascript:window.location.href='${pageContext.request.contextPath}/user/security/sendSmsCode'">
			        发送验证码</span>
			      <br><input type="submit" value="提 交">
			    </form>
			  </div>
			</body>
			
GET /user/security --> 页面如下

点击发送验证码, 再输入验证码提交。这是一个正常的流程。
下面来执行这样一个流程, 首先重启服务器 ( 必须重启 ), 不点击发送验证, 随便输入内容或不输入内容直接提交, 这时候报 HTTP Status 500 - Expected session attribute 'smsCode'
这是因为, 处理方法 verifyPhone 中的 @ModelAttribute("smsCode") String smsCode 引起的。
当不点击发送验证码时, 处理方法 sendSmsCode 没有执行, 那么这时候, @SessionAttributes("smsCode") smsCode 属性尚未被初始化, 这时候又期望通过 @ModelAttribute("smsCode") 从 session 中取得 smsCode 属性的值, 这样就会出问题。
将 input.jsp 中的 <%-- <form action="${pageContext.request.contextPath}/user/security/verifyMobile" method="post"> --%> 注释取消, 将下一行注释起来。再走一次同样的流程, 无需重启服务器, 发起 GET /user/security 请求, 不点击发送验证, 随便输入内容或不输入内容直接提交, 走 verifyMobile 处理方法, 这时候流程回归正常。
这里主要是通过 ModelMap 来处理, 除了 request 域, session 域里的属性, ModelMap 一样可以访问的到, 这样就可以防止 @SessionAttributes 空值引发的 500 异常。

示例代码下载
spring-mvc-session-attributes.zip