我们在使用 Struts2 的时候一定有用过它的 ModelDriven 类型的 Action,它适于把零散的属性自动组装到一个 JavaBean 中,它的 Model 就相当于 Struts1 的 FormBean。你的 Action 必须实现 com.opensymphony.xwork2.ModelDriven 接口及它的 getModel() 方法,当然要声明一个存储 Model 对象的属性,这个属性将会被压入到 ValueStack 中。
你也可以自然或不自觉在 Action 中加入一个 setModel() 方法,试图在执行期修改当前的 Model 对象,比如像下面这样的 IndexAction 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package cc.unmi.struts2; import com.opensymphony.xwork2.ModelDriven; public class IndexAction implements ModelDriven<User> { private User user = new User("Fantasia"); @Override public User getModel() { return this.user; } public void setModel(User user){ this.user = user; } public String execute() throws Exception { User newUser = new User("Unmi"); setModel(newUser); //试图设置新的 Model //或者直接用下面的方式想设置新的 Model //this.user = newUser; return "success"; } } |
所引用的 Model 对象 User 的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package cc.unmi.struts2; public class User { private String name; public User(String name){ this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String toString(){ return super.toString()+"@Name:"+this.name; } } |
现在你来到转向的 jsp 页面里,用 Struts2 标签:
<s:property value="name"/>
来显示值,想想是 "Fantasia" 还是 "Unmi" 呢?--------- 是 "Fantasia”。
因为 property 标签显示的是 ValueStack 中根对象的属性值,根对象 Model 仍然是原来那个 user 对象。要是你使用:
<s:property value="model.name"/>
显示出来的正是设置进去的新的 Model 对象的 newUser 的 name 属性,显示为 "Unmi"。为什么呢,因为它找到的是根对象 IndexAction 的属性对象 model 的 name 属性。
我们用 <s:debug/> 看一下当前 ValueStack 中的状况:
旧的 User(name:Fantasia) 对象放在了 ValueStack 的根上了,而后来的 newUser(name:Unmi) 也确实被 Action 的 model 重新引用了。
这也是源自于我们的一种惯性思维,太清晰的把 JSP 分为前台,Action 部分为后台,好像是未显示前面之前可以随意改其中欲显示的内容,而实际并非总是如此。
原因是 ModelDrivenInterceptor 在 setModel() 之前已经调用 getModel() 把原来的 Model 对象放到了 ValueStack 的根上了,而后来的 setModel() 只是改变了 Action 的 model 的引用,并未修改在 ValueStack 根位置的 Model 的引用。
如果你用 Spring 来管理 Struts2 的 Action,要是没有设置 scope="prototype",则会出现第二次访问时 ValueStack 中的 Model 被成功替换的假象,原因是共用了前一个 Action 实例,当然拦截器在调用 getModel() 时会使用后面设置的 newUser。
那么我们怎么才能替换掉 ValueStack 中的 Model 对象,让 <s:property value="name"/> 能取到新设置 Model 对象的值呢?由于我们很难让 ValueStack 中的 Model 引用再指向别的对象,但可以改变其中的值,像:
getModel().setName("Another Name");
这样是对的,Model 中有许多的属性值,一个个设置是不太可能的,这时修我们可以自定义 setModel() 方法了,借助于 apache commons 或是 Spring 提供的 BeanUtils.copyProperties() 方法,它们的参数顺序不一样,且 Spring 的是抛出的 RuntimeException:
org.springframework.beans.BeanUtils.copyProperties(source, target);
org.apache.commons.beanutils.BeanUtils.copyProperties(dest, orig);
如果用 Spring 的 BeanUtils,那么此时的 setModel() 可写成:
1 2 3 |
public void setModel(User user){ org.springframework.beans.BeanUtils.copyProperties(this.user, user); } |
用 apache commons 的 BEanUtils,需捕获异常,写成(注意,这个是泛型版本):
1 2 3 4 5 6 7 |
public void setModel(M model) { try { org.apache.commons.beanutils.BeanUtils.copyProperties(this.model, model); } catch (Exception e) { log.error("Set model values error.",e); } } |
写到这里,基本是大功告成了,研究的乐趣在什么地方呢?就是不断有新的风景,见第二个图有个判断:
1 2 3 |
if (refreshModelBeforeResult) { invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model)); } |
进到 RefreshModelBeforeResult 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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; } } // 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); } } } } |
显然,只要设置了 modelDriven 的 refreshModelBeforeResult 属性为 true,使能让普通的 setModel 方法:
1 2 3 |
public void setModel(M model){ this.model = model; } |
直接替换掉 ValueStack 根位置的 Model 的需求了。看下 JavaDoc 中对 refreshModelBeforeResult 的解释吧:
refreshModelBeforeResult - set to true if you want the model to be refreshed on the value stack after action execution and before result execution. The setting is useful if you want to change the model instance during the action execution phase, like when loading it from the data layer. This will result in getModel() being called at least twice.
下面的问题变成了如何配置已定义拦截器的属性了,这里就此打住,不再发挥了,还会另立专题讲述 Struts2 中如何覆盖已内置拦截器的属性(参数)值。
本文链接 https://yanbin.blog/struts2-modeldriven-setmodel-nothing/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
受教了,收藏