相信熟悉 Struts1 的程序员,对 Struts2 会迷惑,凡事是是而非。我也曾经遇到了这种情况。Struts2 在设计的时候采用 webwork 的内核,尽量按照 Struts1 的编码习惯。
我不知道各位怎么学习 Struts1,当我阅读了核心控制器 org.apache.struts.action.ActionServlet 的源码后,感到对 Struts1 的工作机制豁然开朗。 Struts2 同样也是 MVC 框架,但核心控制器是过滤器 org.apache.struts2.dispatcher.FilterDispatcher。
感谢网上对 Struts2 工作机制研究并且愿意跟大家分享的热心人,我在学习 Struts2 的时候得到很多帮助。如果你不是很有经验的程序员,我说的很多东西你可能立刻理解不了。如果有时间,我会做成 ppt,也希望给大家讲解,共同交流进步。
如果你需要亲自动手实践,学习源码,请下载以下的 2 个 jar 包。
工作流程的官方描述
我们从看官方的流程图开始。当本篇文章结束的时候,我们会再一遍来看它。
备注:拦截和过滤器的执行顺序可能一些人理解不了,我以生活中的范例说明。我去上海的 IBM 实验室出差,火车沿途停靠蚌埠,南京,最终达到上海。办完事情后回来,沿途的停靠站是南京、蚌埠。有没有注意到火车停靠站的顺序相反了。好,转到我们遇到的技术问题,上海的业务相当于 Action 执行,是调用的真正目标。蚌埠和南京是两个分别的过滤器。即使我两次路过南京,只是一个过滤器的调用先执行一半后执行一半罢了。
回页首
核心控制器 org.apache.struts2.dispatcher.FilterDispatcher
filter 是否可以作为控制器
传统的 Java MVC 设计模式,控制器天然是 servlet。也许有人说,没有 servlet 还叫 MVC 结构吗?对 filter 作为控制器表示怀疑。filter 为什么不可以做控制器,动态网页也可以做控制器?我不知道如果你开发 PHP 项目,MVC 你怎么处理的,但是我认为答案是肯定的。
请看下面的例子,过滤器实现控制器。核心方法 doFilter 的处理有 3 个出口。
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); } } } |
FilterDispatcher 的工作流程
前面讲过 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() 方法的几点说明 :
<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> |
下边,我们将讨论 dispatcher 类。
回页首
org.apache.struts2.dispatcher.Dispatcher
Dispatcher 做为实际派发器的工具类,委派大部分的处理任务。核心控制器持有一个本类实例,为所有的请求所共享。本部分分析了两个重要方法。
serviceAction():加载 Action 类,调用 Action 类的方法,转向到响应结果。响应结果指代码清单 5 中 <result/> 标签所代表的对象。
cleanup():释放所有绑定到 dispatcher 实例的资源。
serviceAction 方法
根据 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); } } |
几点说明:
cleanup 方法
释放所有绑定到 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; } |
几点说明:
回页首
拦截器与 ActionContext
代理模式与切面
代理模式有 3 个角色:
我们以买笔记本电脑为例
抽象主题为抽象类或接口,定义了 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 方法中实现增强。
好,在本部分总结的末尾,我再强调一遍概念:动态代理中的代理角色 = 切面 = 拦截器。
回页首
关于 valueStack 的讨论
前面我们提到一个概念,value Stack 包含两个部分。但是书上也说,很多时候或者是通常特指 Object Stack,用术语说就是 OGNL value stack。怎么理解 ?
|--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)。
如果你可以完全看懂上面的图,那你可以省略这一部分。但是好在本部分都是精华的,而且不多。
最后一个问题,通常我们编写 Struts2 只有一个过滤器 FilterDispatcher,为什么这边是三个过滤器 ?
SiteMesh 可以对你编写的页面进行装饰,以美化界面,当然笔者的界面恰好属于一般般,刚脱离丑的那种类型。如果 SiteMesh 要访问值栈 value stack,原来清除值栈的工作由 FilterDispatcher 完成。org.apache.struts2.dispatcher.ActionContextCleanUp 告诉 FilterDispatcher 不要清除值栈,由自己来清除。
参考资料
学习
获得产品和技术
讨论