6. ModelDriven拦截器、Preparable 拦截器

1. 问题

  • Struts2 的 Action 我们将它定义为一个控制器,但是由于在 Action 中也可以来编写一些业务逻辑,也有人会在 Action 输入业务逻辑层。
  • 但是在企业开发中,我们一般会将业务逻辑层单独编写,而不是将它与 action 层写到一起。
  • 之前的练习中,我们一直将属性如 username 、 password 等保存在了 action 中。
  • 这样做了以后导致我们在调用业务逻辑层时可能需要将Action的对象传过去。
  • 但是这样做无异于直接将 Servlet 传给 Service,导致两层之间的耦合,而且我们也不希望其他对象可以直接使用Action对象。
  • 要解决这个问题,我们可以采用的一种方式是,专门在Action中定义一个属性,用来封装信息。然后只需要将这个对象传个service即可。
  • 但是这样又带来了一个新问题,谁来将属性封装进对象呢?答案就是ModelDriven拦截器
  • 拦截器:
    • 拦截器的作用和过滤器类似。
    • 拦截器可以在请求到达Action之前进行拦截,希望在请求到达Action之前通过拦截器做一些准备工作。

  • Struts2 简单的运行流程:
    • 请求首先到达StrutsPrepareAndExecuteFilter.doFilter()
    • 在doFilter方法中,先获取ActionMapping
      • 判断:如果ActionMapping为null,不是Struts请求,直接放行
      • 如果ActionMapping不为null,是Struts请求,继续处理
    • 通过configurationManager加载Struts的配置信息,找到请求对应的Action对象
      • 根据配置信息创建ActionProxy代理类。
    • StrutsActionProxy.execute()方法中调用DefaultActionInvocation.invoke()方法
    • 对所有的拦截器进行迭代在去分别调用拦截器intercept方法,进行拦截请求
    • intercept方法我们对请求进行一些处理,处理完毕以后继续DefaultActionInvocation.invoke()方法
    • 如此反复直到所有的拦截器都被调用
    • 最后才去执行Action的方法。
Struts2 运行流程 | center

2. 请求参数在哪封装的:

  • 请求参数在到达Action之前,会先经过ParametersInterceptor拦截器,
  • 在该拦截器中会自动对将请求参数封装进值栈的栈顶对象,
  • 他会根据属性名将属性值设置进栈顶对象的相应属性中,
  • 如果栈顶中没有该属性,则继续向第二对象进行封装。
<!-- 查看 ParametersInterceptor 源码-->
@Override
public String doIntercept(ActionInvocation invocation) throws Exception {
    Object action = invocation.getAction();
    if (!(action instanceof NoParameters)) {
        ActionContext ac = invocation.getInvocationContext();
        final Map<String, Object> parameters = retrieveParameters(ac);

        if (LOG.isDebugEnabled()) {
            LOG.debug("Setting params " + getParameterLogMap(parameters));
        }

        if (parameters != null) {
            Map<String, Object> contextMap = ac.getContextMap();
            try {
                ReflectionContextState.setCreatingNullObjects(contextMap, true);
                ReflectionContextState.setDenyMethodExecution(contextMap, true);
                ReflectionContextState.setReportingConversionErrors(contextMap, true);

                ValueStack stack = ac.getValueStack();
                setParameters(action, stack, parameters); // 在这方法中设置参数到对象中
            } finally {
                ReflectionContextState.setCreatingNullObjects(contextMap, false);
                ReflectionContextState.setDenyMethodExecution(contextMap, false);
                ReflectionContextState.setReportingConversionErrors(contextMap, false);
            }
        }
    }
    return invocation.invoke();
}

3. ModelDriven拦截器

  • 虽然我们知道了参数是从什么地方压入栈顶对象的,但是我们还是无法更好的处理方式把参数封装成对象,放到对象中去。
  • 所以我们需要使用ModelDriven 拦截器
  • 我们查看 ModelDrivenInterceptor 源码能发现,在该拦截器中,对于实现了 ModelDriven 接口的Action ,会调用getModel() 方法把,getModel() 返回的对象压入值栈栈顶,所以对于我们来说,可以使用一个JavaBean 作为参数的分装
@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();
}
  • 当用户触发 add 请求时, ModelDriven 拦截器将调用 EmployeeAction 对象的 getModel() 方法, 并把返回的模型(Employee实例)压入到 ValueStack 栈.
  • 接下来 Parameters 拦截器将把表单字段映射到 ValueStack 栈的栈顶对象的各个属性中. 因为此时 ValueStack 栈的栈顶元素是刚被压入的模型(Employee)对象, 所以该模型将被填充. 如果某个字段在模型里没有匹配的属性, Param 拦截器将尝试 ValueStack 栈中的下一个对象
/**
 * 实现 ModelDriven 接口
 */
public class EmployeeAction extends ActionSupport implements RequestAware, ModelDriven<Employee> 

//=================

// 重写实现 getModel 方法
@Override
public Employee getModel() {

    System.out.println("  getModel() Method, ............");

    if (employee == null) {
        employee = new Employee();
    } 
    // 如果通过 return new Employee() 的方式,虽然栈顶对象是Employee,但是在Action 中的Employee为Null
    return employee;
}
  • 但是我们还有一个问题需要解决, 我们去修改表单数据的时候。通常需要使用Id 去数据库中查询出来对象。如果我们直接用下面的方式,在修改页面是无法获取到对应的值。
    • 示例中我们只是使得 employee 引用复制了一份,并且指向了给 employeeDao.getEmpById(employee.getId());对象,实际上我们在栈顶兑现给的值并没有改变
public String input() {
    // 因为点击修改的时候,会put Id值到栈顶对象Employee 中,所以我们查询出来的employee 并不会放到栈顶
    // 如果这样的话 我们把指向 ValueStack 栈顶的引用,改为了从数据库查询出来的值。
    // 所以 ValueStack 中的 employee 并没有被赋值
    employee = employeeDao.getEmpById(employee.getId());
    
    System.out.println(employee);
    
    return "input";
}
  • 如图所示| center
  • 但是我们如果一个个的去赋值属性又特别麻烦.。如果使用获取ValueStack 的方式去重新放入栈顶再弹出的方式也不是一个好的方法

Employee emp = employeeDao.getEmpById(employee.getId());
employee.setName(emp.getName());
employee.setRole(emp.getRole());
employee.setDept(emp.getDept());
employee.setAge(emp.getAge());
  • 所以我们最好的方式,是在 getModel() 方法中,根据 Action 的属性 id 去数据库中获取资源。但是程序中默认使用的是 defaultStack 拦截器栈。在defaultStack拦截器栈中,modelDriven 拦截器之前并没有 params 拦截器,如果需要对Action 参数赋值的话,就需要在 modelDriven 拦截器之前设置params 拦截器
 <interceptor-stack name="defaultStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="alias"/>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="i18n"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="chain"/>
    <interceptor-ref name="scopedModelDriven"/>
    <interceptor-ref name="modelDriven"/>
    <interceptor-ref name="fileUpload"/>
    <interceptor-ref name="checkbox"/>
    <interceptor-ref name="datetime"/>
    <interceptor-ref name="multiselect"/>
    <interceptor-ref name="staticParams"/>
    <interceptor-ref name="actionMappingParams"/>
    <interceptor-ref name="params"/>
    <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-ref name="debugging"/>
    <interceptor-ref name="deprecation"/>
</interceptor-stack>
  • 根据上面的结果,我们需要做的操作是:
    1. 先调用 param 拦截器封装一部分属性进入Action
    2. 再调用modelDriven 拦截器进入值栈
    3. 在 getModel() 方法中判断id 是否为空,然后再返回不同的结果
  • 于是,我们可以使用 paramsPrepareParamsStack 拦截器栈,paramsPrepareParamsStack 这个拦截器栈中params拦截器会执行两次,
    • 一次在ModelDriven之前,一次在ModelDriven之后,
    • 这样我们就可以根据不同的请求参数,给值栈栈顶放入不同对象。
  • 在 struts.xml 修改默认的拦截器栈
<!-- 修改拦截器栈 -->
<interceptors>
    <interceptor-stack name="myInterceptorStck" >
        <interceptor-ref name="paramsPrepareParamsStack"></interceptor-ref>
    </interceptor-stack>
</interceptors>
    
<!-- 设置使用的拦截器为 我们设置的拦截器栈: myInterceptorStck  -->
<default-interceptor-ref name="myInterceptorStck"></default-interceptor-ref>
  • 使用paramsPrepareParamsStack 拦截器栈后,修改后的 getModel()input 方法
@Override
public Employee getModel() {
    
    System.out.println(" getModel() Method ............");

    if (id == null) {
        employee = new Employee();
    } else {
        employee = employeeDao.getEmpById(id);
    }
    return employee;
}

// ====================
public String input() {
    return "input";
}
  • 这样使得代码就更加的简洁了一些
  • 流程

4. 新的问题

  • 在使用 paramsPrepareParamsStack 拦截器栈 之后,虽然已经很简洁了,但是我们出现了新的问题。
    • 在我们做 CRUD 操作是, deleteupdate 方法都带有 id
      • 一个根据 id 删除用户
      • 一个是更新用户信息
    • 都有 id 就会导致调用getModel会去数据库中查询员工信息
    • 但是实际业务上我们并没有这样的需求
    • 所以我们引出了 Preparable拦截器

5. Preparable 拦截器

  • Struts 2.0 中的 modelDriven 拦截器负责把 Action 类以外的一个对象压入到值栈栈顶
  • 而 prepare 拦截器负责准备为 getModel() 方法准备 model, 并且在 modelDriven 拦截器之前
  • PrepareInterceptor拦截器使用方法
    • 若 Action 实现 Preparable 接口,则 Action 方法需实现 prepare() 方法
    • PrepareInterceptor 拦截器将调用 prepare() 方法,prepareActionMethodName()方法 或 prepareDoActionMethodName ()方法
    • PrepareInterceptor 拦截器根据 firstCallPrepareDo 属性决定获取 prepareActionMethodName 、prepareDoActionMethodName 的顺序。默认情况下先获取 prepareActionMethodName (), 如果没有该方法,就寻找prepareDoActionMethodName()。如果找到对应的方法就调用该方法
    • PrepareInterceptor 拦截器会根据 alwaysInvokePrepare 属性决定是否执行 prepare() 方法
// com.opensymphony.xwork2.interceptor.PrepareInterceptor
private final static String PREPARE_PREFIX = "prepare";
private final static String ALT_PREPARE_PREFIX = "prepareDo";

private boolean alwaysInvokePrepare = true;
private boolean firstCallPrepareDo = false;
//===============
@Override
public String doIntercept(ActionInvocation invocation) throws Exception {
    Object action = invocation.getAction();

    if (action instanceof Preparable) {
        try {
            String[] prefixes;
             //根据 firstCallPrepareDo 的值(默认为false)来决定调用两个prepare 方法,可以在struts.xml 中修改
            if (firstCallPrepareDo) {
                prefixes = new String[] {ALT_PREPARE_PREFIX, PREPARE_PREFIX};
            } else {
                prefixes = new String[] {PREPARE_PREFIX, ALT_PREPARE_PREFIX};
            }
            //调用
            PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes);
        }
        catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof Exception) {
                throw (Exception) cause;
            } else if(cause instanceof Error) {
                throw (Error) cause;
            } else {
                throw e;
            }
        }

        // 根据 alwaysInvokePrepare  值(默认为true)来决定是否调用 Action 的 prepare()方法
        if (alwaysInvokePrepare) {
            ((Preparable) action).prepare();
        }
    }

    return invocation.invoke();
}
  • 方法执行顺序:
    • action.prepareXxx方法
    • action.prepare方法
    • action.getModel
    • action.action方法
  • 我们可以在prepareXxx方法做一个个性化的处理,可以在prepare方法做一些统一的处理
public String add() {
    employeeDao.save(employee);
    return SUCCESS;
}
/**
 * 该方法会在add方法执行之前执行
 * 也会在getModel方法执行之前执行,放到值栈栈顶中
 */
public void prepareDoAdd() {
    System.out.println("prepareDoAdd............");
    employee = new Employee();
}

//======================================================
public String del() {
    employeeDao.delete(employee.getId());
    return SUCCESS;
}
//======================================================
public String input() {
    return "input";
}
public void prepareInput() {
    System.out.println("prepareInput.........");
    employee = employeeDao.getEmpById(id);
}
//======================================================
public String update() {
    employeeDao.update(employee);
    return SUCCESS;
}
/**
 * 在update方法之前,初始化employ,然后由 getModel 方法放到栈顶
 */
public void prepareUpdate() {
    System.out.println("prepareUpdate..............");
    employee = new Employee();
}
//======================================================
@Override
public Employee getModel() {
    System.out.println("getModel。。。。。。。。。。。");
    return employee;
}
//======================================================
/**
 * 该方法中,可以做一些统一的处理
 */
@Override
public void prepare() throws Exception {
    
    System.out.println("prepare..................");
    
    /*
     *  例如:对于 delete 方法,虽然该方法只需要使用 id, 
     *      但是我们调用该方法之前,调用 getModel() 返回的是个 null
     *      所以我们可以在这 做一次判断
     */
    if (employee == null) {
        employee = new Employee();
    }
}
  • 对于我们 如果想修改 PrepareInterceptor拦截器 中的一些参数。
    • 修改 prepareXxx 和 prepareDoXxxx 调用的顺序
    • 修改 alwaysInvokePrepare 的值,使得 Action 不调用 prepare() 方法
    • 参考 docs 中 Interceptor Parameter Overriding
<!-- 修改拦截器栈 -->
<interceptors>
    <interceptor-stack name="myInterceptorStck" >
        <interceptor-ref name="paramsPrepareParamsStack">
            <!-- 修改拦截器栈属性值,name.filed -->
            <param name="prepare.alwaysInvokePrepare">false</param>
            <param name="prepare.firstCallPrepareDo">true</param>
        </interceptor-ref>
    </interceptor-stack>
</interceptors>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 标签 如果要配置的标签,那么必须要先配置标签,代表的包的概念。 包含的属性 name包的名称,要求是唯一的,管理a...
    偷偷得路过阅读 1,312评论 0 0
  • 概述 什么是Struts2的框架Struts2是Struts1的下一代产品,是在 struts1和WebWork的...
    inke阅读 2,242评论 0 50
  • action中如何接受页面传过来的参数 第一种情况:(同名参数) 例如:通过页面要把id=1 name=tom a...
    清枫_小天阅读 2,936评论 1 22
  • 两节细胞生物学复习课 以前上课不认真,基本上算是废的,复习课上着比正常上课还困难 集中不了注意力,决定自己看书 2...
    伤心不再来and快乐永存阅读 272评论 0 0