Struts2对应MVC的C。控制层

一、Struts1  VS  Struts2

  1.控制器:Struts2使用一个Filter作为控制器,StrutsPrepareAndExecuteFilter

       Struts1使用ActionServlet作为控制器

  2.表单映射:Struts2表单直接被映射到POJO

        Struts1每一个表单都对应一个ActionForm实例

  3.Action类:Struts2任何一个POJO都可以是一个Action类

        Struts1中Action类必须继承org.apache.struts.action.Action

  4.前端页面:Struts2可以使用OGNL表达式

  5.验证逻辑:Struts2的验证逻辑卸载Action类中

        Struts1的验证逻辑必须写在ActionForm的实现类中

 

二、HelloWorld

1.web.xml

配置Filter,StrutsPrepareAndExecuteFilter,拦截所有请求

    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

 

2.struts.xml(classpath下)

<struts>
  <constant name="struts.action.extension" value="action, ,do"></constant> 可以处理的action请求的扩展名 <package name="helloword" extends="struts-default"> <action name="information-save" class="com.helloworld.Information" method="save"> <result name="details">/jsp/details.jsp</result> </action> </package> </struts>

3.Action类

public class Information {
    private Integer id;
    private String infoName;
    private String infoDesc;
    
    @Override
    public String toString() {
        return "Information [id=" + id + ", infoName=" + infoName
                + ", infoDesc=" + infoDesc + "]";
    }
    public Information() {
        super();
    }
    public Information(String infoName, String infoDesc) {
        super();
        this.infoName = infoName;
        this.infoDesc = infoDesc;
    }
    
    public String save() {
        System.out.println("save():"+this);return "details";
    }
    

}

4.jsp

<form action="information-save.action" method="post">
  informationName:<input type="text" name="infoName"/><br>
  informationDesc:<input type="text" name="infoDesc"/><br>
<input type="submit" value="提交" />

 

三、Action类

(一)Action类的特点

Action类可以是任何一个POJO

必须有一个空参的构造器

Form表单提交数据之后,表单的属性映射到Action类的相应属性上,因此,属性必须一一对应,可以是任意类型,基本类型和字符串之间可以自动类型转换,其它要使用类型转换器(后面讲)

必须有一个可以供Struts2 在执行这和action时可调用的方法(这个方法在struts.xml中配置),用于应答action请求,这个方法必须返回一个String,在struts.xml的配置中,根据该返回值返回对应的jsp

一个Action类中,可以都多个action方法

Struts2为每一个action请求创建一个Action实例,因此,Action在多线程下是安全的(由于Action不是单例的,所以,在SSH整合的时候,将Action实例交由Spring IOC容器创建,在Spring配置文件中配置bean时,bean的作用范围必须指定为原型的,默认是单例的)

(二)在Action中访问web资源(Struts2访问Servlet)

HttpSession   HttpServletRequest HttpServletResponse 

1.与Servlet API 耦合的方式

能访问到更多的原生方法,通过ServletActionContext对象 或者 实现XxxAware接口(servletRequestAware ServletResponseAware ServletContextAware)

 (1)ServletActionContext对象

如何获取session:

在获取request之后,调用request.getSession()

    public static ServletContext getServletContext() {
        return (ServletContext) ActionContext.getContext().get(SERVLET_CONTEXT);
    }
    public static HttpServletResponse getResponse() {
        return (HttpServletResponse) ActionContext.getContext().get(HTTP_RESPONSE);
    }
    public static HttpServletRequest getRequest() {
        return (HttpServletRequest) ActionContext.getContext().get(HTTP_REQUEST);
    }

 

2.与Servlet API 解耦的方式

只能访问到较少的方法,通过ActionContext对象 或者 实现XxxAware接口(ApplicationAware  RequestAware  SessionAware  ParameterAware)

(1)ActionContext对象

Action的上下文对象,类似于PageContext ServletContext

ActionContext  context = ActionContext.getContext();

调用ActionContext对象的getXxx方法,获取对应的Map。Struts2构造了三个Map对象,用于封装Application(ServletContext)、session、parameter

  如何获取Request,调用get("request")

public Object get(String key) {
       return context.get(key);
}

    public Map<String, Object> getApplication() {
        return (Map<String, Object>) get(APPLICATION);
    }
    public Map<String, Object> getParameters() {
        return (Map<String, Object>) get(PARAMETERS);
    }
    public Map<String, Object> getSession() {
        return (Map<String, Object>) get(SESSION);
    }

 (2)XxxAware接口

public class TestAwareAction implements ApplicationAware{

    public String execute(){
        applicationMap.put("Aware_application", "Aware_application_value");
        System.out.println(applicationMap.get("dateJsp"));
        return "success";
    }

    private Map<String, Object> applicationMap;
    @Override
    public void setApplication(Map<String, Object> applicationMap) {
        this.applicationMap = applicationMap;
    }
}

 

四、ActionContext类

Action的上下文对象,保存了一个Action在执行过程中所必须的对象,session, parameters, locale等,Struts2会根据每个执行HTTP请求的下城创建一个对应的ActionContext对象,每一个线程对应一个ActionContext对象

获取每一个线程对应的ActionContext对象-----------ActionContext context = ActionContext.getContext();(ActionContext对象有一个静态成员变量ThreadLocal,在getContext方法中调用ThreadLocal对象的get方法,获取当前线程的ActionContext对象,不用担心线程安全问题)

Action是线程安全的,原因---------ActionContext对象的成员变量:static ThreadLocal actionContext = new ThreadLocal();

ActionContext是用来存放Action在执行过程中所必须的对象,有struts2本身存放的数据,也可以在程序中向其中存放数据

实际是一个Map<String, Object>

创建ActionContext对象的时机-----------发送请求,请求被StrutsPrepareAndExecuteFilter拦截,调用拦截器的doFilter方法,在该方法中创建ActionContext对象

如何创建-----------调用prepare.createActionContext(request, response);     看是否已经有与当前线程绑定的ActionContext对象。有,根据之前的ActionContext对象创建一个新的ActionContext对象;没有,获取ValueStack对象,根据值栈中保存的键值对创建一个ActionContext对象。将此次创建的ActionContext对象放入ThreadLocal中。

    public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
        ActionContext ctx;
        Integer counter = 1;
        Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
        if (oldCounter != null) {
            counter = oldCounter + 1;
        }
        
        ActionContext oldContext = ActionContext.getContext();
        if (oldContext != null) { 
            // detected existing context, so we are probably in a forward
            ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap()));
        } else {
            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); //创建值栈
    public ValueStack createValueStack() {
        ValueStack stack = new OgnlValueStack(xworkConverter, compoundRootAccessor, textProvider, allowStaticMethodAccess);
        container.inject(stack);
        stack.getContext().put(ActionContext.CONTAINER, container);
        return stack;
    }
            stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext));
            ctx = new ActionContext(stack.getContext());
        }
        request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
        ActionContext.setContext(ctx);
    public static void setContext(ActionContext context) {
        actionContext.set(context); //static ThreadLocal actionContext = new ThreadLocal();  调用ThreadLocal的set方法,将ActionContext对象放在当前线程的ThreadLocal中
    }

return ctx;
    }

 

 

 五、ActionSupport类

  <default-class-ref class="com.opensymphony.xwork2.ActionSupport" />

  如果在配置<action>时,没有指定class属性的值,ActionSupport的全类名就是该属性的值(默认值),此时,execute方法即为默认要执行的action方法

 

六、Result接口

 public void execute(ActionInvocation invocation) throws Exception;

每一个action方法都有一个String返回值,根据String返回值决定响应什么结果

<result >是<action >的子节点,可以包含多个<result>

<result>的属性 name type

name:结果的名字,必须和action方法的String返回值对应,默认success

type:结果的响应类型,默认dispatcher

<result-types>
<result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/>    构成一个chain链,前一个action将控制权交给后一个action,通过dispatcher不能实现chain
<result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/>  将控制权转发给当前应用程序内部的一个资源
<result-type name="freemarker" class="org.apache.struts2.views.freemarker.FreemarkerResult"/>
<result-type name="httpheader" class="org.apache.struts2.dispatcher.HttpHeaderResult"/>
<result-type name="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult"/>    重定向 可以是外部资源
<result-type name="redirectAction" class="org.apache.struts2.dispatcher.ServletActionRedirectResult"/>    重定向到另一个Action,通过redirect可以实现redirectChain
<result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/>
<result-type name="velocity" class="org.apache.struts2.dispatcher.VelocityResult"/>
<result-type name="xslt" class="org.apache.struts2.views.xslt.XSLTResult"/>
<result-type name="plainText" class="org.apache.struts2.dispatcher.PlainTextResult" />
<result-type name="postback" class="org.apache.struts2.dispatcher.PostbackResult" />
</result-types>

七、通配符

  一个<action>可以处理多个action请求,减少<action>的配置

如果有多个相匹配的,则没有通配符的那个胜出;如果匹配到的多个都有通配符,则写在最前面的胜出

匹配到的URI字符串的子串可以用{1}{2}{3}……进行引用,{1}对应第一个通配符,以此类推

*  匹配0到多个字符,不包含“/”

若要匹配“/” 使用**

转义使用“/”

        <action name="employee-*" class="com.duan.action.InterceptionEmployeeAction" method="{1}">
            <result name="{1}">/interception/employee-{1}.jsp</result>
     </action>

 

八、值栈

  ValueStack接口   OgnlValueStack

创建时机-----------在Filter的doFilter方法中,调用prepare.createActionContext(request, response);创建ActionContext对象,在该方法中,先判断是否已经存在与当前线程绑定ActionContext对象,不存在时,先创建调用ValueStackFactory的createValueStack方法创建值栈,然后根据值栈创建ActionContext对象

    public ValueStack createValueStack() {
        ValueStack stack = new OgnlValueStack(xworkConverter, compoundRootAccessor, textProvider, allowStaticMethodAccess);
        container.inject(stack);
        stack.getContext().put(ActionContext.CONTAINER, container);
        return stack;
    }

Struts2中,在jsp页面的隐含对象request不是tomcat创建的原生的HttpServletRequest,而是Struts2包装之后的StrutsRequestWrapper,重写了getAttribute方法,先获取ActionContext对象,从ActionContext对象中获取ValueStack对象,从ValueStack对象中获取属性值

    public Object getAttribute(String s) {
        if (s != null && s.startsWith("javax.servlet")) {
            // don't bother with the standard javax.servlet attributes, we can short-circuit this
            // see WW-953 and the forums post linked in that issue for more info
            return super.getAttribute(s);
        }

        ActionContext ctx = ActionContext.getContext();
        Object attribute = super.getAttribute(s);
        if (ctx != null) {
            if (attribute == null) {
                boolean alreadyIn = false;
                Boolean b = (Boolean) ctx.get("__requestWrapper.getAttribute");
                if (b != null) {
                    alreadyIn = b.booleanValue();
                }
    
                // note: we don't let # come through or else a request for
                // #attr.foo or #request.foo could cause an endless loop
                if (!alreadyIn && s.indexOf("#") == -1) {
                    try {
                        // If not found, then try the ValueStack
                        ctx.put("__requestWrapper.getAttribute", Boolean.TRUE);
                        ValueStack stack = ctx.getValueStack();
                        if (stack != null) {
                            attribute = stack.findValue(s);
                        }
                    } finally {
                        ctx.put("__requestWrapper.getAttribute", Boolean.FALSE);
                    }
                }
            }
        }
        return attribute;
    }
}

每个Action对象都拥有一个ValueStack

OgnlValueStack

CompoundRoot root;   public class CompoundRoot extends ArrayList,虽然root是一个ArrayList,但是内部的pop push方法实现了栈的先进先出的功能
transient Map<String, Object> context;

root:栈。存储属性值。Struts2默认将Action及其相关对象放在栈顶,在调用ActionProxyFactory的createActionProxy方法时,会创建ActionInvocation对象,在调用该对象的init方法时,将Action对象放入值栈栈顶

stack.push(action);
contextMap.put("action", action);

context:键值对。存储各种键值对,request、session、application、attr、parameters

 

 

九、OGNL表达式

在jsp页面,使用ognl表达式获取值栈中存放的数据

1.读入root中的对象

(1)某个对象的某个属性值

Object.propertyName

Object["propertyName"]

Object['propertyName']

(2)可以使用下标

[0].propertyName  返回栈顶对象的属性值

使用下标[i].propertyName,如果在下标为i的对象中没有找到指定的属性,则从i开始,往后找

若从栈顶元素往后找某个属性的值,可以省略下标,直接指定属性值即可

<s:property value="message"> 从栈顶元素开始找message属性的值,这是最常用的方法

2.读取context(Map)中的属性

加上前缀字符“#”

(1)某个对象的某个属性值

#Object.propertyName

#Object["propertyName"]

#Object['propertyName']

(2)使用Map中键的引用

#request  #application #session #attr

eg:#request.code  返回request域中code属性的值

3.调用字段和方法

可以使用OGNL表达式调用任何一个类的静态的字段和方法,或者一个压入root中的对象的公共字段和方法

需要修改默认配置,使得Struts2支持该功能  allowStaticMethodAccess

eg:

<s:property  value="@java.lang.Math@PI" >

<s:property  value="@java.lang.Math@cos(0)" >

<s:property value="[0].infoName">

<s:property value="[0].setInfoName("sss")">

 

十、exception拦截器-----ExceptionMappingInterceptor------默认拦截器栈defaultStack

  程序发生异常时,返回异常页面

全局(全局的result属性必须引用全局<result>)

局部

<s:property value="exception">

<s:property value="exception.message">

    <global-results>
        <result name="error">/error.jsp</result>
    </global-results>
    <global-exception-mappings>
        <exception-mapping result="error" exception="java.lang.ArithmeticException"></exception-mapping>
    </global-exception-mappings>
        <action name="information-save" class="com.helloworld.Information" method="save">
            <exception-mapping result="input" exception="java.lang.ArithmeticException"></exception-mapping>
            <result name="input">/jsp/input.jsp</result>
            <result name="details">/jsp/details.jsp</result>
        </action>

 

 

十一、params拦截器-----ParametersInterceptor------默认拦截器栈defaultStack

将表单字段映射到值栈栈顶对象的同名属性中,如果栈顶对象不存在同名属性,尝试栈中的下一个对象

例如,删除一条记录,传递记录的id值,在Action类中创建一个同名的属性,setXxx(),即可

通常情况下,值栈的栈顶对象都是Action类的对象,因此,会将表单字段映射到Action类的同名属性中,如果表单字段过多,就会导致Action类庞大

例如,插入一条记录,要将记录的每一个属性值通过表单传递,此时,要在Action类中创建记录的每一个属性对应的属性及其setXxx(),将Action类和Model耦合在一起

需要将Action和Model分开

 

十二、modelDriven拦截器----ModelDrivenInterceptor------默认拦截器栈defaultStack

使Action和Model分开

Action类实现ModelDriven接口,modelDriven拦截器就会将getModel方法返回的那个对象置于值栈栈顶,接下来params拦截器就会将表单字段映射到值栈栈顶元素的同名属性中,如果栈顶元素不存在同名属性,则去栈中下一个对象中找

例如,插入一个记录,创建一个记录对应的实体类,在getModel方法中返回该实体类的一个对象即可

public interface ModelDriven<T> {

    /**
     * Gets the model to be pushed onto the ValueStack instead of the Action itself.
     *
     * @return the model
     */
    T getModel();

}
    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        Object action = invocation.getAction();

        if (action instanceof ModelDriven) {    //判断当前Action类有没有实现ModelDriven接口,如果实现了,就会获取重写的抽象方法getModel的返回值,将其置于值栈栈顶
            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();
    }

 

  表单回显

    在值栈栈顶有一个对象,如果该对象的属性与表单的name属性是同名属性,就会自动回显

  

十三、paramsPrepareParamsStack拦截器栈的使用

更新一条记录时,首先要传递记录的id,到达编辑页面,需要将记录的原始信息显示在页面上,更新之后,提交

params拦截器 - modelDriven拦截器 - params拦截器          paramsPrepareParamsStack拦截器栈可以实现

更换拦截器栈,struts.xml    <default-interceptor-ref name="paramsPrepareParamsStack"></default-interceptor-ref>

 

十四、prepare拦截器----PrepareInterceptor------paramsPrepareParamsStack拦截器栈

Action类必须实现Preparable接口,重写prepare方法

modelDriven拦截器将一个Action类以外的一个对象压入值栈的栈顶,而prepare拦截器负责为getModel方法提供model

通过分析prepare拦截器的执行过程,会调用两个方法 prepareXxx 和 prepareDoXxx,如果Action类中存在这两个方法,会调用,然后根据配置决定是否调用重写的prepare方法

流程:如果Action类实现了Preparable接口,就会触发prepare拦截器的doIntercept方法,默认情况下,尝试执行prepareXxx方法,若该方法存在,执行,尝试执行prepareDoXxx方法,若该方法存在,执行;之后,根据配置决定是否执行重写的prepare方法

为需要的action方法创建一个配对的prepareXxx方法,并设置不执行prepare方法

如何设置拦截器的参数? 

        <interceptors>
            <interceptor-stack name="duanStack">
                <interceptor-ref name="paramsPrepareParamsStack">
                    <param name="prepare.alwaysInvokePrepare">false</param>
                </interceptor-ref>
            </interceptor-stack>
        </interceptors>

        <default-interceptor-ref name="duanStack"></default-interceptor-ref>

 

public interface Preparable {

    /**
     * This method is called to allow the action to prepare itself.
     *
     * @throws Exception thrown if a system level exception occurs.
     */
    void prepare() throws Exception;
    
}
    public String doIntercept(ActionInvocation invocation) throws Exception {
        Object action = invocation.getAction();

        if (action instanceof Preparable) {  //判断Action类是否实现了Preparable接口
            try {
                String[] prefixes;
                if (firstCallPrepareDo) {  //根据拦截器的firstCallPrepareDo属性的值(默认false),确定prefix(方法前缀)
  private final static String PREPARE_PREFIX = "prepare";
    private final static String ALT_PREPARE_PREFIX = "prepareDo";
                    prefixes = new String[] {ALT_PREPARE_PREFIX, PREPARE_PREFIX};
                } else {
                    prefixes = new String[] {PREPARE_PREFIX, ALT_PREPARE_PREFIX};
                }
                PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes);    //调用前缀方法

    public static void invokePrefixMethod(ActionInvocation actionInvocation, String[] prefixes) throws InvocationTargetException, IllegalAccessException {
        Object action = actionInvocation.getAction();  //获取Action类的实例
        
        String methodName = actionInvocation.getProxy().getMethod();  //获取对应的action方法的名字
        
        if (methodName == null) {
            // if null returns (possible according to the docs), use the default execute 
            methodName = DEFAULT_INVOCATION_METHODNAME;    //如果action方法的名字是空,证明没有配置,使用默认的execute方法
        }
        
        Method method = getPrefixedMethod(prefixes, methodName, action);  //获取前缀方法

    public static Method getPrefixedMethod(String[] prefixes, String methodName, Object action) {
        assert(prefixes != null);
        String capitalizedMethodName = capitalizeMethodName(methodName);  //将目标action方法的方法名的首字母大写
        for (String prefixe : prefixes) {
            String prefixedMethodName = prefixe + capitalizedMethodName;  //这个的值会是prepareXXX   prepareDoXXX
            try {
                return action.getClass().getMethod(prefixedMethodName, EMPTY_CLASS_ARRAY);
            }
            catch (NoSuchMethodException e) {
                // hmm -- OK, try next prefix
                if (LOG.isDebugEnabled()) {
                    LOG.debug("cannot find method [" + prefixedMethodName + "] in action [" + action + "]");
                }
            }
        }
        return null;
    }

if (method != null) {
            method.invoke(action, new Object[0]);  //通过反射调用前缀方法
        }
    }


            }
            catch (InvocationTargetException e) {
                /*
                 * The invoked method threw an exception and reflection wrapped it
                 * in an InvocationTargetException.
                 * If possible re-throw the original exception so that normal
                 * exception handling will take place.
                 */
                Throwable cause = e.getCause();
                if (cause instanceof Exception) {
                    throw (Exception) cause;
                } else if(cause instanceof Error) {
                    throw (Error) cause;
                } else {
                    /*
                     * The cause is not an Exception or Error (must be Throwable) so
                     * just re-throw the wrapped exception.
                     */
                    throw e;
                }
            }

            if (alwaysInvokePrepare) {  //根据alwaysInvokePrepare属性决定是否调用Action类中重写的Preparable接口的prepare方法
                ((Preparable) action).prepare();
            }
        }

        return invocation.invoke();
    }

 

十五、类型转换

(一)类型转化错误时,如何处理?
  若Action类没有实现ValidationAware接口,在类型转化错误时,struts2会继续调用action方法,将该类型转化错误的属性的属性值置为默认值,不报错。
  若Action类实现了ValidationAware接口,在类型转化错误时,struts2会检查当前<action>是否配置了<result name="input">……</result>,若配置了,将控制权交给该<result>;若没有配置,报错:No result defined for action …… and result input。

(二)如何显示类型转化失败时的错误消息?
  若form标签使用的是默认的主题(xhtml),则自动显示错误消息,默认的错误消息是:Invalid field value for field ……
  若form标签使用的是simple主题,使用<s:fielderror>标签显示。例如:<s:fielderror fieldName="name"/>

(三)默认的错误消息是如何显示的?
  如果当前Action类实现了ValidationAware接口,conversionError拦截器(默认拦截器栈的一员)负责添加与类型转化相关的错误消息。

(四)如何覆盖、定制默认的错误消息
  在当前字段的model所在的包下新建一个文件,文件名:字段所在类的类名.properties
  在该新建的文件中输入键值对,如下
    invalid.fieldvalue.表单中相应字段的name属性的值=定制的错误消息

(五)自定义类型转化器

  1.为什么要自定义?
    params拦截器只能完成基本数据类型和字符串之间的类型转化,不能完成字符串和引用类型之间的转换。
    例如:字符串和日期之间的转化
  2.如何自定义?
   (1)开发一个类,继承StrutsTypeConverter
   (2)配置(两种方式)
    ①基于字段的配置(只能处理当前字段的类型装换异常)
      在字段所在的model所在的包下新建一个文件,文件名:字段所在类的类名-conversion.properties
      在该新建文件中输入键值对,如下
        待转换的字段名=自定义类型转换器的全类名
      基于字段配置的自定义类型转化器在第一次使用时创建实例,并且仅创建一次(单例)
    ②基于类型的配置(可以处理当前类型的所有字段的转换异常)
      在类路径下新建一个文件,文件名:xwork-conversion.properties
      在该新建文件中输入键值对,如下
        带转换类型的全类名=自定义类型转换器的全类名
      基于类型配置的自定义类型转化器在当前web应用被加载时创建。