转自:http://www.ibm.com/developerworks/cn/java/j-lo-struct2/index.html
我们从看官方的流程图开始。当本篇文章结束的时候,我们会再一遍来看它。
- 初始的请求通过一条标准的过滤器链,到达 servlet 容器 ( 比如 tomcat 容器,WebSphere 容器 )。
- 过滤器链包括可选的 ActionContextCleanUp 过滤器,用于系统整合技术,如 SiteMesh 插件。
- 接着调用 FilterDispatcher,FilterDispatcher 查找 ActionMapper,以确定这个请求是否需要调用某个 Action。
- 如果 ActionMapper 确定需要调用某个 Action,FilterDispatcher 将控制权交给 ActionProxy。
- ActionProxy 依照框架的配置文件(struts.xml),找到需要调用的 Action 类。
- ActionProxy 创建一个 ActionInvocation 的实例。ActionInvocation 先调用相关的拦截器 (Action 调用之前的部分),最后调用 Action。
- 一旦 Action 调用返回结果,ActionInvocation 根据 struts.xml 配置文件,查找对应的转发路径。返回结果通常是(但不总是,也可能是另外的一个 Action 链)JSP 技术或者 FreeMarker 的模版技术的网页呈现。Struts2 的标签和其他视图层组件,帮助呈现我们所需要的显示结果。在此,我想说清楚一些,最终的显示结果一定是 HTML 标签。标签库技术和其他视图层技术只是为了动态生成 HTML 标签。
- 接着按照相反次序执行拦截器链 ( 执行 Action 调用之后的部分 )。最后,响应通过滤器链返回(过滤器技术执行流程与拦截器一样,都是先执行前面部分,后执行后面部)。如果过滤器链中存在 ActionContextCleanUp,FilterDispatcher 不会清理线程局部的 ActionContext。如果不存在 ActionContextCleanUp 过滤器,FilterDispatcher 会清除所有线程局部变量。
备注:拦截和过滤器的执行顺序可能一些人理解不了,我以生活中的范例说明。我去上海的 IBM 实验室出差,火车沿途停靠蚌埠,南京,最终达到上海。办完事情后回来,沿途的停靠站是南京、蚌埠。有没有注意到火车停靠站的顺序相反了。好,转到我们遇到的技术问题,上海的业务相当于 Action 执行,是调用的真正目标。蚌埠和南京是两个分别的过滤器。即使我两次路过南京,只是一个过滤器的调用先执行一半后执行一半罢了。
核心控制器 org.apache.struts2.dispatcher.FilterDispatcher
传统的 Java MVC 设计模式,控制器天然是 servlet。也许有人说,没有 servlet 还叫 MVC 结构吗?对 filter 作为控制器表示怀疑。filter 为什么不可以做控制器,动态网页也可以做控制器?我不知道如果你开发 PHP 项目,MVC 你怎么处理的,但是我认为答案是肯定的。
请看下面的例子,过滤器实现控制器。核心方法 doFilter 的处理有 3 个出口。
- 请求资源以 .action 结尾,进行 action 处理
- 对样式表的直接访问。如地址栏直接输入网址 /css/main.css 将会被拒绝
- 其它资源请求,不处理请求直接通过
class FilterDispatcher implements Filter { private FilterConfig filterConfig; public void init(FilterConfig filterConfig) throws ServletException {} public void destroy() {} // 核心过滤方法 public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; String uri = req.getRequestURI(); // 1 action 请求 // 可能的 uri 形式为 / 站点名 /resourceName/ 可选路径 /Product_input.action if (uri.endsWith(".action")) { int lastIndex = uri.lastIndexOf("/"); //1.1 处理 action 结尾的请求 String action = uri.substring(lastIndex + 1); if (action.equals("Product_input.action")) { //1.1.1 请求商品输入不做处理 } else if (action.equals("Product_save.action")) { Product product = new Product(); //1.1.2 保存商品信息 product.setProductName(request.getParameter("productName")); product.setDescription(request.getParameter("description")); product.setPrice(request.getParameter("price")); product.save(); request.setAttribute("product", product); } //1.2 转向视图 String dispatchUrl = null; if (action.equals("Product_input.action")) { dispatchUrl = "/jsp/ProductForm.jsp"; } else if (action.equals("Product_save.action")) { dispatchUrl = "/jsp/ProductDetails.jsp"; } if (dispatchUrl != null) { RequestDispatcher rd = request .getRequestDispatcher(dispatchUrl); rd.forward(request, response); } } else if (uri.indexOf("/css/") != -1 && req.getHeader("referer") == null) { //2 拒绝对样式表的直接访问 res.sendError(HttpServletResponse.SC_FORBIDDEN); } else { //3 请求其他资源,通过过滤器 filterChain.doFilter(request, response); } } } |
前面讲过 Struts2 的核心控制器为 filter,对于一个控制器,核心的生命周期方法有 3 个。
// 初始化,加载资源 public void init(FilterConfig filterConfig) throws ServletException // 销毁,回收资源 public void destroy() // 过滤 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException |
分别讲解 FilterDispatcher 3 个方法
init 方法:初始化过滤器,创建默认的 dispatcher 对象并且设置静态资源的包。
public void init(FilterConfig filterConfig) throws ServletException { try { this.filterConfig = filterConfig; // 初始化日志器 initLogging(); dispatcher = createDispatcher(filterConfig); dispatcher.init(); dispatcher.getContainer().inject(this); staticResourceLoader.setHostConfig(new FilterHostConfig(filterConfig)); } finally { ActionContext.setContext(null); } } |
destory 方法:核心业务是调用 dispatcher.cleanup() 方法。cleanup 释放所有绑定到 dispatcher 实例的资源,包括销毁所有的拦截器实例,本方法在后面有源代码讨论。
public void destroy() { if (dispatcher == null) { log.warn("something is seriously wrong, Dispatcher is not initialized (null) "); } else { try { dispatcher.cleanup(); } finally { ActionContext.setContext(null); } } } |
doFilter 方法:doFilter 方法的出口有 3 个分支。
首先过滤器尝试把 request 匹配到一个 Action mapping(action mapping 的解释见最后的总结)。若有匹配,执行 path1。否则执行 path2 或者 3。
path 1调用 dispatcher. serviceAction() 方法处理 Action 请求
如果找到了 mapping,Action 处理被委托给 dispatcher 的 serviceAction 方法。 如果 Action 处理失败了,doFilter 将会通过 dispatcher 创建一个错误页。
path 2处理静态资源
如果请求的是静态资源。资源被直接拷贝到 response 对象,同时设置对应的头信息。
path 3无处理直接通过过滤器,访问过滤器链的下个资源。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; ServletContext servletContext = getServletContext(); String timerKey = "FilterDispatcher_doFilter: "; try { //1 处理前的准备 //1.1 创建值栈对象,值栈包含 object stack 和 context map 两个部分。 ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); //1.2 创建 actionContext。 ActionContext ctx = new ActionContext(stack.getContext()); ActionContext.setContext(ctx); UtilTimerStack.push(timerKey); //1.3 准备和包装 request request = prepareDispatcherAndWrapRequest(request, response); ActionMapping mapping; //2 根据请求路径查找 actionMapping try { mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager()); } catch (Exception ex) { log.error("error getting ActionMapping", ex); dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex); return; } //3 当请求路径没有对应的 actionMapping,走第 2 和第 3 个出口 if (mapping == null) { String resourcePath = RequestUtils.getServletPath(request); if ("".equals(resourcePath) && null != request.getPathInfo()) { resourcePath = request.getPathInfo(); } if (staticResourceLoader.canHandle(resourcePath)) { staticResourceLoader.findStaticResource(resourcePath, request, response); } else { chain.doFilter(request, response); } // 如果是第 2 和第 3 个出口 ,action 的处理到此结束。 return; } //3.1 路径 1,委托 dispatcher 的 serviceAction 进行处理 dispatcher.serviceAction(request, response, servletContext, mapping); } finally { try { //4 清除 ActionContext ActionContextCleanUp.cleanUp(req); } finally { UtilTimerStack.pop(timerKey); } } } |
对 doFilter() 方法的几点说明 :
- valueStack 的建立是在 doFilter 的开始部分,在 Action 处理之前。即使访问静态资源 ValueStack 依然会建立,保存在 request 作用域。
- ValueStack 在逻辑上包含 2 个部分:object stack 和 context map,object stack 包含 Action 与 Action 相关的对象。context map 包含各种映射关系。request,session,application,attr,parameters 都保存在 context map 里。
parameters: 请求参数
atrr: 依次搜索 page, request, session, 最后 application 作用域。
图 3. 值栈
- 准备和包装 request,包含 2 步 :
准备请求,设置区域和字符编码
包装 request,如果包含文件上传,则网页表单设置为 multipart/form-data,返回 MultiPartRequestWrapper 类型。
如果普通表单提交,返回 StrutsRequestWrapper。这个类是 multipart/form-data 的父类。 - ActionMapping 代表 struts.xml 文件中的一个 Action 配置,被传入到 serviceAction 中。注意 ActionMapping 不代表 Action 集合,只代表某个对应的 Action。
清单 6. action 配置
<action name="Pay" class=" "> <interceptor-ref name="tokenSession" / > <interceptor-ref name="basicStack" / > <result name="input">/jsp/Payment.jsp</result> <result>/jsp/Thanks.jsp</result> </action>
- 如果是一个 Action 请求,( 请求路径在 struts.xml 有对应的 Action 配置 ),则调用 dispatcher.serviceAction() 处理。
- finally 语句块中调用 ActionContextCleanUp.cleanUp(req),以清除 ActionContext。
下边,我们将讨论 dispatcher 类。
org.apache.struts2.dispatcher.Dispatcher
Dispatcher 做为实际派发器的工具类,委派大部分的处理任务。核心控制器持有一个本类实例,为所有的请求所共享。 本部分分析了两个重要方法。
serviceAction():加载 Action 类,调用 Action 类的方法,转向到响应结果。响应结果指代码清单 5 中 <result/> 标签所代表的对象。
cleanup():释放所有绑定到 dispatcher 实例的资源。
根据 action Mapping 加载 Action 类,调用对应的 Action 方法,转向相应结果。
首先,本方法根据给定参数,创建 Action context。接着,根据 Action 的名称和命名空间,创建 Action 代理。( 注意这代理模式中的代理角色 ) 然后,调用代理的 execute() 方法,输出相应结果。
如果 Action 或者 result 没有找到,将通过 sendError() 报 404 错误。
public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context, ActionMapping mapping) throws ServletException { Map<String, Object> extraContext = createContextMap (request, response, mapping, context); //1 以下代码目的为获取 ValueStack,代理在调用的时候使用的是本值栈的副本 ValueStack stack = (ValueStack) request.getAttribute (ServletActionContext.STRUTS_VALUESTACK_KEY); boolean nullStack = stack == null; if (nullStack) { ActionContext ctx = ActionContext.getContext(); if (ctx != null) { stack = ctx.getValueStack(); } } //2 创建 ValueStack 的副本 if (stack != null) { extraContext.put(ActionContext.VALUE_STACK, valueStackFactory.createValueStack(stack)); } String timerKey = "Handling request from Dispatcher"; try { UtilTimerStack.push(timerKey); //3 这个是获取配置文件中 <action/> 配置的字符串,action 对象已经在核心控制器中创建 String namespace = mapping.getNamespace(); String name = mapping.getName(); String method = mapping.getMethod(); // xwork 的配置信息 Configuration config = configurationManager.getConfiguration(); //4 动态创建 ActionProxy ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class). createActionProxy(namespace, name, method, extraContext, true, false); request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack()); //5 调用代理 if (mapping.getResult() != null) { Result result = mapping.getResult(); result.execute(proxy.getInvocation()); } else { proxy.execute(); } //6 处理结束后,恢复值栈的代理调用前状态 if (!nullStack) { request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack); } } catch (ConfigurationException e) { //7 如果 action 或者 result 没有找到,调用 sendError 报 404 错误 if(devMode) { LOG.error("Could not find action or result", e); } else { LOG.warn("Could not find action or result", e); } sendError(request, response, context, HttpServletResponse.SC_NOT_FOUND, e); } catch (Exception e) { sendError(request, response, context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } finally { UtilTimerStack.pop(timerKey); } } |
几点说明:
- Valuestack 对象保存在 request 里,对应的 key 是 ServletActionContext.STRUTS_VALUESTACK_KEY。调用代理之前首先创建 Valuestack 副本,调用代理时使用副本,调用后使用原实例恢复。本处的值栈指 object stack。
- Dispatcher 实例,创建一个 Action 代理对象。并把处理委托给代理对象的 execute 方法。
- 如果你不懂代理模式,那么在下一部分,我会简单介绍这种模式。但本处使用的是动态代理。动态代理,指代理类和代理对象都是动态生成的,不需要编写类的源代码。
- createContextMap 的创建,建议去看一下。代码很简单。
释放所有绑定到 dispatcher 实例的资源
public void cleanup() { //1 销毁 ObjectFactory ObjectFactory objectFactory = getContainer().getInstance(ObjectFactory.class); if (objectFactory == null) { LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed"); } if (objectFactory instanceof ObjectFactoryDestroyable) { try { ((ObjectFactoryDestroyable)objectFactory).destroy(); } catch(Exception e) { LOG.error(" exception occurred while destroying ObjectFactory ["+objectFactory+"]", e); } } //2 为本线程销毁 Dispatcher 实例 instance.set(null); //3 销毁 DispatcherListeners(Dispatcher 监听器 )。 if (!dispatcherListeners.isEmpty()) { for (DispatcherListener l : dispatcherListeners) { l.dispatcherDestroyed(this); } } //4 调用每个拦截器的 destroy() 方法,销毁每个拦截器 Set<Interceptor> interceptors = new HashSet<Interceptor>(); Collection<Interceptor> packageConfigs = configurationManager. getConfiguration().getPackageConfigs().values(); for (PackageConfig packageConfig : packageConfigs) { for (Object config : packageConfig.getAllInterceptorConfigs().values()) { if (config instanceof InterceptorStackConfig) { for (InterceptorMapping interceptorMapping : ((InterceptorStackConfig) config).getInterceptors()) { interceptors.add(interceptorMapping.getInterceptor()); } } } } for (Interceptor interceptor : interceptors) { interceptor.destroy(); } //5 销毁 action context ActionContext.setContext(null); //6 销毁 configuration configurationManager.destroyConfiguration(); configurationManager = null; } |
几点说明:
- ObjectFactory 对象工厂,用来创建核心的框架对象,比如拦截器、Action、result 等等。本类处于 xwork 的包中。
- DispatcherListeners 为监听器设计模式,仅仅在 Dispatcher 的 init 和 destory 中执行,也就是说仅仅在 Dispatcher 初始化和销毁的时候动作。
- actionContext 是一个 context,Action 在其中执行。actionContext 从根本上讲是一个容器,action 执行所需要的对象,如会话,请求参数,区域信息,valueStack 等,包含在其中。
- 第 6 步销毁 xwork 配置对象。
代理模式有 3 个角色:
- 抽象主题 (abstract subject)
- 真实主题 (real subject)
- 代理主题 (proxy subject)
我们以买笔记本电脑为例
抽象主题为抽象类或接口,定义了 request() 的行为,就是买电脑。
真实主题为买 hp 笔记本,要调用实现接口的 request() 方法,当然你找不到 hp 公司,你只能找到销售 hp 笔记本的电脑公司。
代理主题为销售 hp 笔记本的电脑公司。这家公司可能会说,今天买电脑都送一台数码相机,也可能跟你打折等等。总之在代理主题角色执行的时候,销售公司可以发生某些行为,发生的这些行为叫增强 advice,增强只能发生在代理角色。
代理模式的使用场景,增强是代理的目的。
public interface Subject { abstract public void request(); } class RealSubject implements Subject { public RealSubject() {} public void request() { System.out.println(" From real subject. "); } } // 代理角色 class ProxySubject implements Subject { private RealSubject realSubject; // 真实主题对象 public ProxySubject() {} public void preRequest() {} public void postRequest() {} public void request() { preRequest(); if (realSubject == null) { realSubject = new RealSubject(); } // 此处执行真实对象的 request 方法 realSubject.request(); postRequest(); } } |
代理角色是切面,preRequest 为前置增强,postRequest 为后置增强。当然切面 aspect 的标准定义为两个要素:增强加切入。
你编写的 preRequest() 和 postRequest() 方法一定会参与到真实主题的的 request() 方法执行中。
假设你还不了解,我想请问,如果有个机会,一个很漂亮的妹妹的 MM 要你帮她买东西,你会不会自己贴点钱,或者说些话,让 MM 觉得开心一些。如果是,你就是切面,你的额外的事情和钱就是切面上的增强。
动态代理中的 代理角色 = 切面 = 拦截器。请看下面的实现。
// 省略 Subject 接口和 RealSubject 类 // 调用处理器的类 class DynamicSubject implements InvocationHandler { private Object sub; public DynamicSubject() {} public DynamicSubject(Object obj) { sub = obj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(" before calling " + method); method.invoke(sub, args); System.out.println(" after calling " + method); return null; } } // 客户类 class Client { static public void main(String[] args) throws Throwable { RealSubject rs = new RealSubject(); // 真实主题 InvocationHandler ds = new DynamicSubject(rs); Class cls = rs.getClass(); // 生成代理对象 Subject subject = (Subject) Proxy.newProxyInstance( cls.getClassLoader(), cls.getInterfaces(), ds); subject.request(); } } |
动态代理必须依赖于反射。动态代理,代理类和代理对象都是运行时生成的 (runtime),所以称为动态代理。InvocationHandler 实现类的原代码参与到代理角色的执行。一般在 Invoke 方法中实现增强。
好,在本部分总结的末尾,我再强调一遍概念:动态代理中的代理角色 = 切面 = 拦截器。
- Dispatcher 类的 serviceAction() 方法中,执行处理的核心语句为 proxy.execute()。
- ActionProxy 类,也就是代理角色,持有 ActionInvocation 的实例引用。ActionInvocation 代表着 Action 执行的状态,它持有着拦截器和 Action 实例的引用。ActionInvocation 通过反复调用 invoke() 方法,调用沿着拦截器链向下走。走完拦截器链后运行 Action 实例,最后运行 Result。
- Result 往往对应网页 (jsp,freemarker,velocity),网页的结果被写到输出流里。客户端接收到请求的网页。
- 你看到 postRequest() 方法了吗,它在真实主题结束后运行。这也决定了,为什么 Action 运行结束后,控制权还要按照拦截器链返回。
前面我们提到一个概念,value Stack 包含两个部分。但是书上也说,很多时候或者是通常特指 Object Stack,用术语说就是 OGNL value stack。怎么理解 ?
OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL),通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。
|--application | |--session context map---| |--value stack(root) | |--request | |--parameters | |--attr (searches page, request,session, then application scopes) |
说我的结论,然后再看原代码。
Struts2 框架,把 ActionContext 设置为 OGNL 上下文。ActionContext 持有 application,session,request,parameters 的引用。ActionContext 也持有 value stack 对象的引用 ( 注意这个时候 value stack 特指 Object stack)。
上述对象的引用,ActionContext 不直接持有,而是通过自己的属性 Map<String, Object> context 持有引用。处理 OGNL 表达式最顶层的对象是 Map<String, Object> context。
public static final String ACTION_NAME = "com.opensymphony.xwork2.ActionContext.name"; /** * 值栈在 map 的 key,map 肯定是 key-value 对结构了,别说你不知道。map 指本类最后一个属性 context。 */ public static final String VALUE_STACK = ValueStack.VALUE_STACK; /** * session 的 key,以下省略 */ public static final String SESSION = "com.opensymphony.xwork2.ActionContext.session"; public static final String APPLICATION = "com.opensymphony.xwork2.ActionContext.application"; public static final String PARAMETERS = "com.opensymphony.xwork2.ActionContext.parameters"; public static final String ACTION_INVOCATION = "com.opensymphony.xwork2.ActionContext.actionInvocation"; //map 的定义 Map<String, Object> context; public ActionInvocation getActionInvocation() { return (ActionInvocation) get(ACTION_INVOCATION); } // 对作用域对象的引用 public Map<String, Object> getApplication() { return (Map<String, Object>) get(APPLICATION); } public void setSession(Map<String, Object> session) { put(SESSION, session); } public Map<String, Object> getSession() { return (Map<String, Object>) get(SESSION); } // 对 valueStack 的引用 public void setValueStack(ValueStack stack) { put(VALUE_STACK, stack); } public ValueStack getValueStack() { return (ValueStack) get(VALUE_STACK); } // 最关键的代码 public Object get(String key) { return context.get(key); } public void put(String key, Object value) { context.put(key, value); } } |
那么 value stack 类是什么样子呢?值栈是一个数据结构的栈。所有的数据都保存在 root 对象中。
public interface ValueStack { public abstract CompoundRoot getRoot(); // 省略细节 public abstract Object peek(); public abstract Object pop(); public abstract void push(Object o); } public class CompoundRoot extends ArrayList { // 省略细节 } |
你所编写的 Action 类实例,被放在 value stack 里。OGNL 访问 Action 实例的属性,可以省略 #。如果使用了 #, 表示所查找的对象不在 root 里,而在其他位置,比如 session。
在 Action 里如果访问 session ?最直接的方式是使用 ActionContext 得到。第二种方式是实现 SessionAware 接口。
我在总结之前还是希望大家看一下官方的流程图(图 2)。
如果你可以完全看懂上面的图,那你可以省略这一部分。但是好在本部分都是精华的,而且不多。
- FilterDispatcher 接到请求,查找对应的 Action Mapping,调用 Dispatcher 类的 serviceAction() 方法。
- Dispatcher 类的 serviceAction() 方法中创建并且调用 ActionProxy。
- ActionProxy,持有 ActionInvocation 的实例引用。ActionInvocation 代表着 Action 执行的状态,它持有着拦截器和 Action 实例的引用。ActionInvocation 通过反复调用 invoke() 方法,调用沿着拦截器链向下走。
- 走完拦截器链后运行 Action 实例,最后运行 Result。
- 大家注意到拦截器链了吗?它才是 Struts2.0 的核心所在。
最后一个问题,通常我们编写 Struts2 只有一个过滤器 FilterDispatcher,为什么这边是三个过滤器 ?
SiteMesh 可以对你编写的页面进行装饰,以美化界面,当然笔者的界面恰好属于一般般,刚脱离丑的那种类型。如果 SiteMesh 要访问值栈 value stack,原来清除值栈的工作由 FilterDispatcher 完成。org.apache.struts2.dispatcher.ActionContextCleanUp 告诉 FilterDispatcher 不要清除值栈,由自己来清除。