本文共 20780 字,大约阅读时间需要 69 分钟。
相信熟悉 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 执行,是调用的真正目标。蚌埠和南京是两个分别的过滤器。即使我两次路过南京,只是一个过滤器的调用先执行一半后执行一半罢了。
传统的 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); } } } |
前面讲过 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() 方法的几点说明 :
|
下边,我们将讨论 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 |
几点说明:
释放所有绑定到 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 |
几点说明:
代理模式有 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 方法中实现增强。
好,在本部分总结的末尾,我再强调一遍概念:动态代理中的代理角色 = 切面 = 拦截器。
前面我们提到一个概念,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 |
那么 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 不要清除值栈,由自己来清除。学习
获得产品和技术
讨论
转载地址:http://ujebi.baihongyu.com/