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应用被加载时创建。