struts2 ModelDriven & Prepareable 拦截器
前面对于 Struts2 的开发环境的搭建、配置文件以及 Struts2 的值栈都已经进行过叙述了!这次博文我们讲解利用 Struts2 进行 CURD 操作,体验和 Servlet 的不一样,以此案例理解 ModelDriven 和 Prepareable 拦截器!
案例流程
- 获取数据库数据并将之显示在 employee-show.jsp 页面上
- 在 employee-show.jsp 页面可以添加员工信息到数据库,对现有的员工信息进行编辑以及删除操作
- 在将删除或编辑请求传到 action 方法时且将要操作的员工的 empId 以参数的形式传入
- 对现有员工信息进行编辑的时候需要先将其信息回显到表单再进行编辑
解决思路
Employee-show.jsp
- 首先将所有员工信息获取到并于页面展示,如下 JSP 页面发送请求经由 Action 类处理将所有的员工信息传回页面,在页面进行显示(为了方便我们将所有数据存入一个 Map 对象中,使用 Dao 类进行处理)
- emp-show.jsp 页面中利用 标签处理从 action 方法传回的员工信息的 List,并显示。
- 如上显示页面中每个员工信息行尾都会添加两个操作的超链接分别为 Edit 和 Delete
Employee-edit.jsp
- 点击某员工行后的 edit 超链接,其流程如上显示页面所述。struts.xml 文件中使用的是通配符映射,所以其经过 Action 方法处理将会到达 Employee-edit.jsp 页面。
- 点击edit超链接的时候会将所操作的员工的 id 传入 action 方法,即 edit() 方法,edit() 方法将会从现有的员工信息中获得对应的员工的信息将其回显在 Employee-edit.jsp 的表单上
- 在回显的页面上可以进行修改员工信息,点击提交执行 update() 方法,将更新存入 Map,并跳转到显示页面,实时显示更改的员工信息
emp-delete.action
- 点击员工行后的 delete 超链接,由于删除不需要任何页面,所以执行完删除操作之后跳转到 emp-show.action 显示操作后的员工信息。
- 点击 delete 超链接的时候会将所操作的员工的 Id 传入 action 方法,即 delete() 方法,delete() 方法从员工信息库中删除对应的员工信息然后将重定向到 emp-show.action,显示删除后的员工信息
emp-add.action
- 在 employee-show.jsp 页面的添加表单上填写将要添加的员工信息点击提交后执行 emp-show.action 获取新的员工信息列表并显示
- 点击 submit 后,将员工信息保存到一个新的对象中,执行 add() 方法将新的对象添加到存放用户列表中,再重定向到 emp-show.action,显示新的员工信息
案例目录
- 如上目录,其代码如下 public class Dao { private static Map<String, Employoee> empMap = new LinkedHashMap<String, Employoee>(); //初始化所有的员工信息 static { empMap.put("1001", new Employoee(1001, "ZZ", "XY", "110")); empMap.put("1002", new Employoee(1002, "YS", "JJ", "120")); empMap.put("1003", new Employoee(1003, "JC", "HJ", "119")); empMap.put("1004", new Employoee(1004, "KF", "LT", "10086")); empMap.put("1005", new Employoee(1005, "DX", "ZG", "10000")); } //将所有的员工信息存入 List 以便返回页面 public List<Employoee> getEmployee() { return new ArrayList<Employoee>(empMap.values()); } //根据 empId 获得某一个员工的信息 public Employoee getEmployee(String empId) { return empMap.get(empId); } // 根据 empId 从 Map 集合中删除某一个员工 public void deleteEmp(String empId) { empMap.remove(empId); } // 根据传入的 Employee 对象将其添加到 Map 集合之中 public void addEmp(Employoee employoee) { long sysTime = System.currentTimeMillis(); employoee.setEmpId((int) sysTime); System.out.println(employoee.getEmpId()); empMap.put(String.valueOf(employoee.getEmpId()), employoee); } // 根据传入的 Employee 对象传入更新现有的 Employee 对象 public void updateEmp(Employoee employoee) { empMap.put(String.valueOf(employoee.getEmpId()), employoee); } }
- Employee.java public class Employoee { private Integer empId; private String empName; private String empAddress; private String empNum; @Override public String toString() { return "Employoee{" "empId=" empId ", empName='" empName ''' ", empAddress='" empAddress ''' ", empNum='" empNum ''' '}'; } public Employoee(Integer empId, String empName, String empAddress, String empNum) { this.empId = empId; this.empName = empName; this.empAddress = empAddress; this.empNum = empNum; } public Employoee() { } //getXxx()、setXxx() 方法此处省略 }
- EmployeeCurd.java public class EmployeeCurd implements RequestAware { private Dao dao = new Dao(); private Map<String, Object> requestMap; private Integer empId; private String empName; private String empAddress; private String empNumber; private Employee employee; /*getXxx()、setXxx() 方法此处省略*/ public String delete() { dao.deleteEmp(String.valueOf(empId)); return "delete"; } public String add() { // 将表单的字段值为该类的属性赋值,即可初始化 Employee 对象 Employee employoee = new Employoee(null, empName, empAddress, empNumber); // 调用 Dao 方法将该对象加入 dao.addEmp(employoee); return "add"; } public String show() { // 调用 Dao 的方法获得所有员工信息 List<Employoee> employees = dao.getEmployee(); // 将所有员工信息存入 request 域,并在页面进行显示 requestMap.put("empList", employees); return "show"; } // 获得 WEB 资源 request @Override public void setRequest(Map<String, Object> map) { this.requestMap = map; } }
- Employee-edit.jsp <s:form action="emp-update"> <s:hidden name="empId"></s:hidden> <s:textfield label="EmpName" name="empName"></s:textfield> <s:textfield label="EmpAddress" name="empAddress"></s:textfield> <s:textfield label="EmpNumber" name="empNum"></s:textfield> <s:submit value="Submit"></s:submit> </s:form>
思考
- 如上代码,我们在EmployeeCurd.java 中以 show()、add()、delete() 方法为例,其中在 add() 方法时所使用的 Employee 对象以该类的属性初始化,那么该类的属性是如何被初始化的?执行 delete 方法时传入的 empId 又是如何给对应的属性赋值?
- 这些操作看似我们没有对其进行任何处理,但实际上 struts2 的 params 拦截器为我们将这些都做了,params 拦截器的功能是将表单属性值为栈顶对象的对应的属性赋值,即 add() 方法执行前将表单中对应的字段值赋值给栈顶对象(栈顶对象默认为 Action 类的对象,即 EmployeeCurd 对象)。
- 删除操作时执行 delete() 方法会根据 empId 而去操作对象,这就使得我们需要在执行 delete() 方法前获取到传入的 empId,我们知道 params 拦截器会根据栈顶属性去赋值,但是在默认拦截器栈中 params于 ModelDriven 拦截器其后,这时就需要使用 paramsPrepareParamsStack 拦截器栈,相比于默认拦截器此拦截器会在 ModelDriven 拦截器执行前先去执行一次 params 拦截器,在其后再执行一次 params 拦截器,这样的话 getModel 拦截器就会用到传入的 empId 参数,而我们也可以利用 empId 是否为空压入栈顶对应的对象,即添加操作时需要一个空的对象,更新操作时需要根据 empId 获取到已有对象压入栈顶以便回显
- 在 struts.xml 文件中配置使用 paramsPrepareParamsStack 拦截器栈,如下配置需要在 <default-interceptor-ref name="paramsPrepareParamsStack"></default-interceptor-ref>
回显问题
- 对现有员工信息进行编辑的时候在编辑表单会进行回显,是因为 struts2 的表单标签会自动在值栈中寻找匹配的属性值进行回显,例如执行了 emp-edit.action 执行的后 getModel() 方法会根据 empId 将从 Map 集合中获取到的对象压入栈顶,那么在显示页面的时候会从栈顶获取对应的对象为 struts2 的表单标签赋值
缺点
- EmployeeCurd 和 Employee 类中属性存在冗余,那么我们该如何解决?Struts2 默认的拦截器栈为我们提供了 ModelDriven 拦截器以解决此问题
- 实现
- Action 类实现 ModelDriven
- 使用 ModelDriven 拦截器的优点
- Action 类和 Model 类将不存在冗余,Action 类更加简洁
实现 ModelDriven 接口的 Action 类
代码语言:javascript复制public class EmployeeCurd implements RequestAware, ModelDriven<Employoee> {
private Dao dao = new Dao();
private Map<String, Object> requestMap;
private Employoee employoee;
private Integer empId;
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
public String delete() {
dao.deleteEmp(String.valueOf(empId));
return "delete";
}
public String update() {
dao.updateEmp(employoee);
return "update";
}
public String add() {
dao.addEmp(employoee);
return "add";
}
public String show() {
List<Employoee> employees = dao.getEmployee();
requestMap.put("empList", employees);
return "show";
}
public String edit() {
return "edit";
}
@Override
public void setRequest(Map<String, Object> map) {
this.requestMap = map;
}
@Override
public Employoee getModel() {
/*
* 判断调用该拦截器的时候是 edit 还是 show,这取决于是否有参数 empId,决定了 Employee 对象是新建还是依据 empId 获取
* */
if (empId != null) {
employoee = dao.getEmployee(String.valueOf(empId));
} else {
employoee = new Employoee();
}
return employoee;
}
}
- 详解
- ModelDriven 拦截器使用 getModel() 方法将对应的对象压入栈顶,例如 add() 方法执行的时候 getModel() 方法执行后其栈顶为 employee 对象,这样便可以利用 params 拦截器将表单对应的字段属性值赋给栈顶对象对应的属性值
- 源码解析(ModelDriven 拦截器的 intercept 方法) public String intercept(ActionInvocation invocation) throws Exception { //获取 Action 对象: EmployeeCurd 对象, 此时该 Action 已经实现了 ModelDriven 接口 //public class EmployeeAction implements RequestAware, ModelDriven<Employee> Object action = invocation.getAction(); //判断 action 是否是 ModelDriven 的实例 if (action instanceof ModelDriven) { //强制转换为 ModelDriven 类型 ModelDriven modelDriven = (ModelDriven) action; //获取值栈 ValueStack stack = invocation.getStack(); //调用 ModelDriven 接口的 getModel() 方法 //即调用 EmployeeAction 的 getModel() 方法 /* public Employee getModel() { employee = new Employee(); return employee; } */ Object model = modelDriven.getModel(); if (model != null) { //把 getModel() 方法的返回值压入到值栈的栈顶. 实际压入的是 EmployeeCurd 的 employee 成员变量 stack.push(model); } if (refreshModelBeforeResult) { invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model)); } } return invocation.invoke(); }
CURD 操作进阶之 PrepareInterceptor 拦截器
- 以上实现存在的问题:
- 在进行删除操作的时候会传入 empId,而 getModel() 方法判断到 empId 不为空,会从 Map 集合中去获取一个 Employee 对象置于栈顶,而对于 delete 操作不需要对象
- 在显示所有员工的时候 getModel() 方法会创建一个空的 Employee 对象置于栈顶,而对于此操作也是没有必要的
- 解决方案 - 使用 PrepareInterceptor 拦截器
- 实现
- Action 类实现 Preparable 接口
- 查看源码 public String doIntercept(ActionInvocation invocation) throws Exception { // 获取 Action 对象 Object action = invocation.getAction(); // 判断其是否实现了 Prepareable 拦截器 if(action instanceof Preparable) { try { // 存取前缀,不是 prepare 就是 prepareDo String[] prefixes; // 判断 firstCallPrepareDo 属性,其默认为 false if(this.firstCallPrepareDo) { // 若 firstCallPrepareDo为 true, prefixes 的值为 prepareDoXxx,prepareXxx prefixes = new String[]{"prepareDo", "prepare"}; } else { // 若 firstCallPrepareDo为 false,prefixes 的值为 prepareXxx ,prepareDoXxx prefixes = new String[]{"prepare", "prepareDo"}; } // 执行 invokePrefixMethod() 方法,方法名为 prefixes 数组的值 PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes); } catch (InvocationTargetException var5) { Throwable cause = var5.getCause(); if(cause instanceof Exception) { throw (Exception)cause; } if(cause instanceof Error) { throw (Error)cause; } throw var5; } // 判断 alwaysInvokePrepare 属性的值,其默认为 true。 // 若其值为 true 则每次都会调用 Action 的 prepare() 方法, if(this.alwaysInvokePrepare) { ((Preparable)action).prepare(); } } return invocation.invoke(); } public static void invokePrefixMethod(ActionInvocation actionInvocation, String[] prefixes) throws InvocationTargetException, IllegalAccessException { // 获取 Action 实例 Object action = actionInvocation.getAction(); // 获取要调用的 Action 的方法的名字 String methodName = actionInvocation.getProxy().getMethod(); if(methodName == null) { methodName = "execute"; } // 获取前缀方法 Method method = getPrefixedMethod(prefixes, methodName, action); if(method != null) { // 若方法不为空则通过反射调用该前缀方法 method.invoke(action, new Object[0]); } } // 获取前缀方法 public static Method getPrefixedMethod(String[] prefixes, String methodName, Object action) { assert prefixes != null; // 把方法的首字母变为大写 String capitalizedMethodName = capitalizeMethodName(methodName); String[] arr$ = prefixes; int len$ = prefixes.length; int i$ = 0; // 遍历前缀数组 while(i$ < len$) { String prefixe = arr$[i$]; // 通过拼接的方式得到前缀加方法名,第一次为 prepareUpdate 第二次为 prepareDoUpdate String prefixedMethodName = prefixe capitalizedMethodName; try { // 通过反射从 action 对象中调用方法,并返回 return action.getClass().getMethod(prefixedMethodName, EMPTY_CLASS_ARRAY); } catch (NoSuchMethodException var10) { if(LOG.isDebugEnabled()) { LOG.debug("cannot find method [#0] in action [#1]", new String[]{prefixedMethodName, action.toString()}); } i$; } } return null; }
- 结论
- 阅读源码可以得知若 Action 实现了 Preparable 接口, 则 Struts 将尝试执行 prepare[ActionMethodName] 方法, 若 prepare[ActionMethodName] 不存在, 则将尝试执行 prepareDo[ActionMethodName] 方法.若都不存在, 就都不执行
- 若 PrepareInterceptor 的 alwaysInvokePrepare 属性为 false, 则 Struts2 将不会调用实现了 Preparable 接口的 Action 的 prepare() 方法,即 prepare() 可以不去实现而为每一个 Action 方法准备一个 prepareXxx 或 prepareDoXxx 方法,然后将 alwaysInvokePrepare 属性设为 false,那么每次执行就不会触发 prepare 方法
- 若实现了此接口,那么每个 prepareXxx 方法就会为对应的 Xxx 方法准备一个 Model,利用 getModel() 方法将其置于栈顶,而不需要 empId 去判断,影响程序效率。说白了就是 prepareXxx 方法是为 getModel 方法准备返回对象的
- 最终代码(EmployeeCurd.java) public class EmployeeCurd implements RequestAware, ModelDriven<Employoee>, Preparable { private Dao dao = new Dao(); private Map<String, Object> requestMap; private Employoee employoee; private Integer empId; public Integer getEmpId() { return empId; } public void setEmpId(Integer empId) { this.empId = empId; } public String delete() { dao.deleteEmp(String.valueOf(empId)); return "delete"; } /* * 更新和添加一样,需要准备一个新的 employee 对象 * */ public String update() { dao.updateEmp(employoee); return "update"; } /* * 准备一个新的 Employee 对象 * */ public void prepareUpdate() { employoee = new Employoee(); } /* * * 添加到栈顶,使用 ModelDriven 拦截器和 paramsPrepareParmas 拦截器栈之后我们利用 ModelDriven 拦截器将 employee 对象添加到 * 栈顶,不需要为 Action 类创建对应的属性,利用 ModelDriven 将对应的对象添加到栈顶之后执行 params 拦截器时便将请求参数和栈顶 * 对象对应的属性赋值,使用了 prepare 拦截器之后我们在执行 ModelDriven 拦截器之前利用 prepare 拦截器准备好 model 不需要在 * ModelDriven 拦截器中创建对应的对象 * */ public String add() { dao.addEmp(employoee); return "add"; } /* * 利用 ModelDriven 和 prepare 拦截器将对应的 model 添加到栈顶之后并利用 params 拦截器为其赋值,填充栈顶对象,执行完所有的 * 拦截器之后执行 add() 方法,此时的 employee 对象便为已经填充的对象 * */ public void prepareAdd() { employoee = new Employoee(); } /* * 将已有的数据显示的时候不需要准备 model,所以不需要准备 prepareXxx 方法 * */ public String show() { List<Employoee> employees = dao.getEmployee(); requestMap.put("empList", employees); return "show"; } public String edit() { return "edit"; } /* * 对现有的内容做出修改的时候需要进行回显,所以需要使用 prepare 拦截器为 ModelDriven 拦截器准备 model,这样的话便可 * 利用现有的对象实现回显(回显就是利用与栈顶对象匹配的元素去回显) * */ public void prepareEdit() { employoee = dao.getEmployee(String.valueOf(empId)); } @Override public void setRequest(Map<String, Object> map) { this.requestMap = map; } /* * 实现 ModelDriven 拦截器,getModel 方法将把返回对象置于栈顶 * */ @Override public Employoee getModel() { return employoee; } /* * 若不设置 PrepareInterceptor 拦截器的 alwaysInvokePrepare 属性为 false,那么每次调用 action 方法都会执行 prepare 方法 * 若设置其为 false,那么每次调用 Action 方法的时候就不会去调用 prepare 方法 * 我们可以为某些 Action 方法实现 prepareXxx 方法,其为私有定制的方法等同于 prepare 方法,其功能和 prepare 方法等效,都是 * 为 modelDriven 拦截器准备 model,然后利用 modelDriven 将 model 放置在栈顶,这样的话 getModel 和 prepare 方法就不需要 * 去判断是新建对象还是从现有的中获取。 * 在 ModelDriven 拦截器之前执行 params 拦截器时是为栈顶对象 Action 类对应的属性赋值,该例中 Action 类的属性只有 empId * */ @Override public void prepare() throws Exception { System.out.println("prepare"); } }
- 此时的 Action 类的 action 方法就非常的简洁,不会在有其他的冗余问题。
- 在 struts.xml 文件中配置 alwaysInvokePrepare 属性为 false,如下: <interceptors> <interceptor-stack name="myInterceptor"> <interceptor-ref name="paramsPrepareParamsStack"> <param name="prepare.alwaysInvokePrepare">false</param> </interceptor-ref> </interceptor-stack> </interceptors> <!--配置默认的拦截器为我们更改后的拦截器栈,同时也需要在 package 标签内部--> <default-interceptor-ref name="myInterceptor"/>