一、Action中User注入问题
Action中可能会经常用到已经登陆的User对象,如果每次都从Session中拿会显得非常繁琐。可以想一种方法,当Action想要获取User对象的时候直接使用,这种方法还是得需要借助拦截器的力量,直接在登录拦截器中实现即可,但是登陆拦截器怎么知道该Action想要获取User对象呢?这就需要给Action加上一个接口,如果该Action是该接口的实现类,则表示该Action想要获取User对象。接口仿照HttpRequestAware接口的形式,名字为用户关注接口:
package com.kdyzm.struts.action.aware; import com.kdyzm.domain.User;
/**
* 用户关注接口,用于Action获取Session中的User对象
* @author kdyzm
*
*/
public interface UserAware {
public void setUser(User user);
}
这样在登陆拦截器中直接进行判断即可,如果用户已经登陆了,而且请求的Action是UserAware的实现类,那么就通过setUser方法将User注入到Action中。这样登录拦截器中的intercept方法就变成了如下的形式:
public String intercept(ActionInvocation invocation) throws Exception {
System.out.println("被登录拦截器拦截!");
Action action=(Action) invocation.getAction();
if(action instanceof LoginAction ||action instanceof RegisterAction){
System.out.println("即将进行登录或者注册,直接放行!");
return invocation.invoke();
}
HttpServletRequest request=ServletActionContext.getRequest();
HttpSession session=request.getSession();
User user=(User) session.getAttribute("user");
if(user==null){
System.out.println("用户未登录,必须先登录再访问其他资源!即将跳转到登陆界面!");
return "toLoginPage";
}else{
System.out.println("用户已经登陆,登录拦截器已经放行!");
//如果用户名不为空,而且实现了UserAware接口,就需要调用该接口中的相应方法给类中的成员变量赋值
//TODO 给Action中User动态赋值的方法
if(action instanceof UserAware){
((UserAware)action).setUser(user);
}
return invocation.invoke();
}
}
二、设计调查页面,设计调查页面几乎是该项目中最复杂的一个页面了在“我的调查”中的其中一个调查栏目中直接单击“设计调查”超链接,就直接跳转到设计调查页面,当然需要在Action将调查对象(Survey对象)先查询出来。完整的页面设计代码如下这个页面设计起来非常困难,我也是写了好几个小时才完成的,因为需要考虑到很多因素,需要一步一步的进行调试才行。
完整代码是:
<%@ page language="java" pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="${pageContext.servletContext.contextPath}/css/header.css" type="text/css">
<style type="text/css">
table{
border: 1px solid black;
border-collapse: collapse;
width: 100%;
}
table tr{
border-collapse: collapse;
}
tr td{
border:1px solid black;
border-collapse: collapse;
}
a{
color: gray;
text-decoration: none;
}
a:HOVER {
color: red;
}
.tdHL{
text-align: left;
border-width: 0px;
}
.tdHR{
text-align: right;
border-width: 0px;
}
.pageTitle{
background-color: #CCF;
}
.questionTitle{
background-color: #CCC;
}
</style>
<title>Insert title here</title>
</head>
<body>
<%@ include file="/header.jsp" %>
<div>
<!-- 先设计一个变量保存住surveyid -->
<s:set value="%{surveyId}" name="id"/>
<table>
<tr>
<td colspan="2">设计调查</td>
</tr>
<tr>
<td class="tdHL">
<!-- 这里使用sturs2标签直接判断图片是否存在! -->
<!-- 在这里加上一个logo标识 -->
<s:if test="isLogoImageExists()">
<img width="40px" alt="这是logo标识" src="<s:url value='%{logoPath}'/>"/>
</s:if>
<s:else>
<!-- 如果图片不存在,则什么都不显示 -->
</s:else>
<s:property value="title"/>
</td>
<td class="tdHR">
<s:a action="SurveyAction_toUploadLogoPage.action" namespace="/">
<s:param name="surveyId" value="%{#id}"></s:param>
增加Logo
</s:a>
<s:a action="SurveyAction_toEditSurveyPage.action" namespace="/">
<s:param name="surveyId" value="%{#id}"></s:param>
编辑调查</s:a>
<s:a action="PageAction_toAddPagePage.action" namespace="/">
<s:param value="%{#id}" name="surveyId"></s:param>
增加页</s:a>
</td>
</tr>
<tr>
<td colspan="2">
<!-- 主干内容开始 -->
<table>
<tr>
<td width="20px"></td>
<td width="*">
<!-- 迭代页面集合 -->
<table>
<s:iterator value="%{pages}" var="page">
<s:set var="pId" value="%{#page.pageId}"></s:set>
<tr>
<td>
<table>
<tr class="pageTitle">
<!-- 页面标题 -->
<td width="40%" class="tdHL">
<s:property value="%{#page.title}"/>
</td>
<td width="60%" class="tdHR">
<s:a action="PageAction_toEditPageTitlePage.action" namespace="/">
<s:param name="pageId" value="%{#pId}"></s:param>
<s:param name="surveyId" value="%{#id}"></s:param>
编辑页面标题</s:a>
<s:a action="PageAction_toSelectTargetPage.action" namespace="/">
<s:param name="pageId" value="%{#pId}"></s:param>
<s:param name="surveyId" value="%{#id}"></s:param>
移动/复制页
</s:a>
<s:a action="QuestionAction_toSelectQuestionTypePage.action" namespace="/">
<s:param name="pageId" value="%{#pId}"></s:param>
<s:param name="surveyId" value="%{#id}"></s:param>
增加问题</s:a>
<s:a action="PageAction_deletePage.action" namespace="/">
<s:param name="pageId" value="%{#pId}"></s:param>
<s:param name="surveyId" value="%{#id}"></s:param>
删除页</s:a>
</td>
</tr>
<tr>
<td colspan="2">
<table>
<tr>
<td width="20px"></td>
<td>
<!-- 迭代问题的集合 -->
<table>
<s:iterator value="%{#page.questions}" var="question">
<s:set var="qid" value="%{#question.questionId}"></s:set>
<!-- 问题题干 -->
<tr class="questionTitle">
<td class="tdHL"><s:property value="%{#question.title}"/></td>
<td class="tdHR">
<s:a action="QuestionAction_editQuestion.action" namespace="/">
<s:param name="pageId" value="%{#pId}"></s:param>
<s:param name="surveyId" value="%{#id}"></s:param>
<s:param name="questionId" value="%{#qid}"></s:param>
编辑问题</s:a>
<s:a action="QuestionAction_deleteQuestion.action" namespace="/">
<s:param name="pageId" value="%{#pId}"></s:param>
<s:param name="surveyId" value="%{#id}"></s:param>
<s:param name="questionId" value="%{#qid}"></s:param>
删除问题</s:a>
</td>
</tr>
<!-- 问题主体,主要涉及的问题就是问题的分类 -->
<tr>
<td colspan="2">
<!-- 定义变量,为当前类型的题型 -->
<s:set value="%{#question.questionType}" var="qt"></s:set>
<!-- 第一种题型:带有单选框或者复选框的
题目标识就是题号小于4,0-3
-->
<s:if test="#qt lt 4">
<s:iterator value="#question.optionTextArr">
<input type='<s:property value="#qt<2?'radio':'checkbox'"/>'>
<s:property/>
<s:if test="#qt==1 || #qt==3">
<br/>
</s:if>
</s:iterator>
<!-- 处理other的情况 -->
<s:if test="#question.other">
<input type='<s:property value="#qt<2?'radio':'checkbox'"/>'>其它
<s:if test="#question.otherType==1">
<input type="text">
</s:if>
<s:elseif test="#question.otherType==2">
<s:select list="#question.otherSelectOptionArr">
</s:select>
</s:elseif>
</s:if>
</s:if>
<!-- 第二种题型,是下拉列表类型的题型 -->
<s:elseif test="#qt==4">
<s:select list="#question.optionTextArr"></s:select>
</s:elseif>
<s:elseif test="#qt==5">
<s:textfield></s:textfield>
</s:elseif>
<!-- 第三种题型,矩阵问题,6,7,8 -->
<s:else>
<table>
<!-- 列头 -->
<tr>
<td></td>
<s:iterator value="#question.matrixColTitleArr">
<td><s:property/></td>
</s:iterator>
</tr>
<!-- 输出N多行 -->
<s:iterator value="#question.matrixRowTitleArr">
<tr>
<td><s:property/></td>
<s:iterator value="#question.matrixColTitleArr">
<td>
<s:if test="#qt==6">
<input type="radio">
</s:if>
<s:elseif test="#qt==7">
<input type="checkbox">
</s:elseif>
<s:elseif test="#qt==8">
<select>
<s:iterator value="#question.matrixSelectOptionArr">
<option>
<s:property/>
</option>
</s:iterator>
</select>
</s:elseif>
</td>
</s:iterator>
</tr>
</s:iterator>
</table>
</s:else>
</td>
</tr>
</s:iterator>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</s:iterator>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>
最核心的代码是对问题种类的判断:
<s:set value="%{#question.questionType}" var="qt"></s:set>
<!-- 第一种题型:带有单选框或者复选框的
题目标识就是题号小于4,0-3
-->
<s:if test="#qt lt 4">
<s:iterator value="#question.optionTextArr">
<input type='<s:property value="#qt<2?'radio':'checkbox'"/>'>
<s:property/>
<s:if test="#qt==1 || #qt==3">
<br/>
</s:if>
</s:iterator>
<!-- 处理other的情况 -->
<s:if test="#question.other">
<input type='<s:property value="#qt<2?'radio':'checkbox'"/>'>其它
<s:if test="#question.otherType==1">
<input type="text">
</s:if>
<s:elseif test="#question.otherType==2">
<s:select list="#question.otherSelectOptionArr">
</s:select>
</s:elseif>
</s:if>
</s:if>
<!-- 第二种题型,是下拉列表类型的题型 -->
<s:elseif test="#qt==4">
<s:select list="#question.optionTextArr"></s:select>
</s:elseif>
<s:elseif test="#qt==5">
<s:textfield></s:textfield>
</s:elseif>
<!-- 第三种题型,矩阵问题,6,7,8 -->
<s:else>
<table>
<!-- 列头 -->
<tr>
<td></td>
<s:iterator value="#question.matrixColTitleArr">
<td><s:property/></td>
</s:iterator>
</tr>
<!-- 输出N多行 -->
<s:iterator value="#question.matrixRowTitleArr">
<tr>
<td><s:property/></td>
<s:iterator value="#question.matrixColTitleArr">
<td>
<s:if test="#qt==6">
<input type="radio">
</s:if>
<s:elseif test="#qt==7">
<input type="checkbox">
</s:elseif>
<s:elseif test="#qt==8">
<select>
<s:iterator value="#question.matrixSelectOptionArr">
<option>
<s:property/>
</option>
</s:iterator>
</select>
</s:elseif>
</td>
</s:iterator>
</tr>
</s:iterator>
</table>
</s:else>
之前就说过,每个问题的位置不能改变,这是因为将会使用该问题的下标得到该问题是什么种类的问题,一种有九种类型的问题,每一种问题都对应一种独一无二的问题类型。
最终设计效果如下图所示:当然如果只是当前阶段的话是没有这种效果的,必须结合之后的添加问题的功能才行。整个页面都是用表格标签进行了嵌套,所以显得比较难看,但是也没有好的方法,如果有时间的话就会对其进行优化。
三、Action中模型赋值问题。(重点)
每一个Action基本上都是BaseAction的子类,继承BaseAction的优点就是不需要每次都实现模型驱动接口并且重写getModel方法了,模型的赋值过程将会在父类中实现,这里当然也会用到泛型。
但是实现了模型驱动接口需要注意一点事项:可能会有数据库中的信息和前端页面显示的信息不一致的情况发生。
首先实现编辑调查功能,小功能非常小,所以略过不提。但是有必要说一下这个过程,因为这是引发该问题的关键
在设计调查页面单击“编辑调查”->请求SurveyAction.toEditSurveyPage()方法,该方法查询数据库,赋值到model->跳转到editSurveyPage.jsp页面回显,这时候诡异的事情就发生了,回显的时候有异常的情况发生。回显的调查标题是“未命名”,但是该调查原来明明有标题是“居民生活水平调查”。为什么会发生这种情况呢?
原因分析:栈顶指针未改变导致的。看一下在编辑调查方法中的代码:
//跳转到编辑调查的页面上去
public String toEditSurveyPage() throws Exception{
this.model=this.surveyService.getModelById(this.model.getSurveyId());
return "toEditSurveyPage";
}
实际上该方法中只有一句代码而已。就是将数据库中查到的对象赋值给model对象。好像这样就将模型给“刷新”了,但是这也只是“好像”而已,实际上并没有刷新model对象。
在栈顶中存放的是旧model的引用地址,说到底model只是一个变量而已,如果改变了model的值,只是将model的指针指向了新的地址,但是栈顶的model对象中的值并没有被改变。只是无法再通过model对象访问到而已。所以如果直接给model对象赋值但是不做其他修改是没有任何意义的。
解决方法:
方法1.再次压栈,示例代码如下:
ActionContext.getContext().getValueStack().push(model);
当然实际上值栈中就会有两份model对象了,这样做的好处就是解决了model赋值的问题,但是也有弊端,每次都需要这么写不嫌麻烦么?而且这么做一点都不“优雅”。
方法2.通过prepare拦截器加上paramsPrepareStack拦截器栈组合完成该项任务。
具体做法是首先将BaseAction实现Prepare接口,然后在Action中重写接口中的方法。伪代码如下:
public void prepareDesignSurvey(){
this.model = xxx ;
}
当然最重要的还是需要改变默认栈为parameterPrepareStack,否则还是没有任何效果。
Action中的目标方法DesignSurvey中直接跳转页面即可,不需要再对model进行修改。
为什么使用这种方法能够解决问题:实际上引发该问题的原因是模型赋值在模型驱动拦截器获取model之后完成的,这样就导致了即使改变model对象也不会改变栈顶指针,如果将两者顺序颠倒一下,即先给模型赋值,然后模型驱动拦截器再取值,这样就没问题了,关键是在合适的位置实现模型赋值,首先确定的是一定是在模型驱动拦截器之前,合适的位置就是prepareInterceptor拦截器,所以实现Prepare接口然后重写方法即可;但是仅仅这么做还是不够的,因为parametersInterceptor在默认拦截器栈中的顺序是在模型驱动拦截器之后,所以在prepareInterceptor拦截器中获取不到Action中的相关参数,一定会引发类似于id can't load的异常 ,解决方法就是使用paramsPreparedStack拦截器栈,该拦截器栈和默认的拦截器栈的唯一区别就是在prepare拦截器之前增加了一个parameter拦截器,正好解决了Action中属性赋值的问题。
下面是两个拦截器栈的定义:
<!-- An example of the paramsPrepareParams trick. This stack is exactly
the same as the defaultStack, except that it includes one extra interceptor
before the prepare interceptor: the params interceptor. This is useful for
when you wish to apply parameters directly to an object that you wish to
load externally (such as a DAO or database or service layer), but can't load
that object until at least the ID parameter has been loaded. By loading the
parameters twice, you can retrieve the object in the prepare() method, allowing
the second params interceptor to apply the values on the object. -->
<interceptor-stack name="paramsPrepareParamsStack">
<interceptor-ref name="exception" />
<interceptor-ref name="alias" />
<interceptor-ref name="i18n" />
<interceptor-ref name="checkbox" />
<interceptor-ref name="multiselect" />
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*,^struts\..*</param>
</interceptor-ref>
<interceptor-ref name="servletConfig" />
<interceptor-ref name="prepare" />
<interceptor-ref name="chain" />
<interceptor-ref name="modelDriven" />
<interceptor-ref name="fileUpload" />
<interceptor-ref name="staticParams" />
<interceptor-ref name="actionMappingParams" />
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*,^struts\..*</param>
</interceptor-ref>
<interceptor-ref name="conversionError" />
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
</interceptor-stack>
方法3.修改模型驱动拦截器,设置刷新model标识为true
模型驱动拦截器代码很短,看看它到底干了什么事:
public class ModelDrivenInterceptor extends AbstractInterceptor { protected boolean refreshModelBeforeResult = false;
public void setRefreshModelBeforeResult(boolean val) {
this.refreshModelBeforeResult = val;
} @Override
public String intercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction(); if (action instanceof ModelDriven) {
ModelDriven modelDriven = (ModelDriven) action;
ValueStack stack = invocation.getStack();
Object model = modelDriven.getModel();
if (model != null) {
stack.push(model);
}
if (refreshModelBeforeResult) {
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
}
}
return invocation.invoke();
} /**
* Refreshes the model instance on the value stack, if it has changed
*/
protected static class RefreshModelBeforeResult implements PreResultListener {
private Object originalModel = null;
protected ModelDriven action; public RefreshModelBeforeResult(ModelDriven action, Object model) {
this.originalModel = model;
this.action = action;
} public void beforeResult(ActionInvocation invocation, String resultCode) {
ValueStack stack = invocation.getStack();
CompoundRoot root = stack.getRoot(); boolean needsRefresh = true;
Object newModel = action.getModel(); // Check to see if the new model instance is already on the stack
for (Object item : root) {
if (item.equals(newModel)) {
needsRefresh = false;
break;
}
} // Add the new model on the stack
if (needsRefresh) { // Clear off the old model instance
if (originalModel != null) {
root.remove(originalModel);
}
if (newModel != null) {
stack.push(newModel);
}
}
}
}
}
在模型驱动拦截器中有个非常重要的属性:refreshModelBeforeResult,该属性是一个标识字段的属性,用于标识是否需要刷新model,什么是刷新model,就是重新获取model的值并压栈,这样最终就能够实现栈顶中的model对象是最新的model对象。
实现原理:执行模型驱动拦截器的时候,会判断refreshModelBeforeResult的值是否为true,如果为true,则给invocation对象添加一个PreResultListener监听器,模型驱动拦截器中有一个静态类RefreshModelBeforeResult实现了该监听器的接口,观察该实现类的类名,翻译成中文就是“在执行Result之前刷新Model对象”,真是一个直白的方法名,我们知道执行结果集是在最后完成的,那么在执行结果集之前刷新Model对象的话就不会出现上述问题了。它实现刷新的原理十分简单,就是先让老的Model对象弹栈,再获取新的model对象并将新的model对象压栈,OK,前端页面获取的一定就是最新的Model对象了。
方法4:使用BeanUtils中的copy属性的方法直接给model对象中的所有属性重新赋值,使用这种方式的好处就是不需要考虑model对象是新的对象还是老的对象,但是有一点不好之处就是该方法底层使用反射技术,效率比较低,而且每次都要写该方法实际上和方法一没有什么差别了,都比较麻烦,而且显得十分的“不优雅”。
四、编辑调查,略。