相信熟悉 Struts1 的程序员,对 Struts2 会迷惑,凡事是是而非。我也曾经遇到了这种情况。Struts2 在设计的时候采用 webwork 的内核,尽量按照 Struts1 的编码习惯。
我不知道各位怎么学习 Struts1,当我阅读了核心控制器 org.apache.struts.action.ActionServlet 的源码后,感到对 Struts1 的工作机制豁然开朗。 Struts2 同样也是 MVC 框架,但核心控制器是过滤器 org.apache.struts2.dispatcher.FilterDispatcher。
感谢网上对 Struts2 工作机制研究并且愿意跟大家分享的热心人,我在学习 Struts2 的时候得到很多帮助。如果你不是很有经验的程序员,我说的很多东西你可能立刻理解不了。如果有时间,我会做成 ppt,也希望给大家讲解,共同交流进步。
如果你需要亲自动手实践,学习源码,请下载以下的 2 个 jar 包。
图 1. 官方源码包
工作流程的官方描述
我们从看官方的流程图开始。当本篇文章结束的时候,我们会再一遍来看它。
图 2. 官方工作流程图
- 初始的请求通过一条标准的过滤器链,到达 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
filter 是否可以作为控制器
传统的 Java MVC 设计模式,控制器天然是 servlet。也许有人说,没有 servlet 还叫 MVC 结构吗?对 filter 作为控制器表示怀疑。filter 为什么不可以做控制器,动态网页也可以做控制器?我不知道如果你开发 PHP 项目,MVC 你怎么处理的,但是我认为答案是肯定的。
请看下面的例子,过滤器实现控制器。核心方法 doFilter 的处理有 3 个出口。
- 请求资源以 .action 结尾,进行 action 处理
- 对样式表的直接访问。如地址栏直接输入网址 /css/main.css 将会被拒绝
- 其它资源请求,不处理请求直接通过
清单 1. 过滤器用作控制器
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 个。
清单 2. 过滤器生命周期方法
// 初始化,加载资源 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 对象并且设置静态资源的包。
清单 3. init 方法
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 实例的资源,包括销毁所有的拦截器实例,本方法在后面有源代码讨论。
清单 4. destory 方法
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无处理直接通过过滤器,访问过滤器链的下个资源。
清单 5. doFilter 方法
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 实例的资源。
serviceAction 方法
根据 action Mapping 加载 Action 类,调用对应的 Action 方法,转向相应结果。
首先,本方法根据给定参数,创建 Action context。接着,根据 Action 的名称和命名空间,创建 Action 代理。( 注意这代理模式中的代理角色 ) 然后,调用代理的 execute() 方法,输出相应结果。
如果 Action 或者 result 没有找到,将通过 sendError() 报 404 错误。
清单 7. serviceAction 方法
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 的创建,建议去看一下。代码很简单。
cleanup 方法
释放所有绑定到 dispatcher 实例的资源
清单 8. cleanup 方法
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 配置对象。
拦截器与 ActionContext
代理模式与切面
图 4. 拦截器是一种代理设计模式的实现
代理模式有 3 个角色:
- 抽象主题 (abstract subject)
- 真实主题 (real subject)
- 代理主题 (proxy subject)
我们以买笔记本电脑为例
抽象主题为抽象类或接口,定义了 request() 的行为,就是买电脑。
真实主题为买 hp 笔记本,要调用实现接口的 request() 方法,当然你找不到 hp 公司,你只能找到销售 hp 笔记本的电脑公司。
代理主题为销售 hp 笔记本的电脑公司。这家公司可能会说,今天买电脑都送一台数码相机,也可能跟你打折等等。总之在代理主题角色执行的时候,销售公司可以发生某些行为,发生的这些行为叫增强 advice,增强只能发生在代理角色。
代理模式的使用场景,增强是代理的目的。
清单 9. 代理模式
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 的标准定义为两个要素:增强加切入。
图 5. 假设你是这么调用
你编写的 preRequest() 和 postRequest() 方法一定会参与到真实主题的的 request() 方法执行中。
假设你还不了解,我想请问,如果有个机会,一个很漂亮的妹妹的 MM 要你帮她买东西,你会不会自己贴点钱,或者说些话,让 MM 觉得开心一些。如果是,你就是切面,你的额外的事情和钱就是切面上的增强。
动态代理中的 代理角色 = 切面 = 拦截器。请看下面的实现。
清单 10. 动态代理的实现
// 省略 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 运行结束后,控制权还要按照拦截器链返回。
图 6. 本部分的总结
关于 valueStack 的讨论
前面我们提到一个概念,value Stack 包含两个部分。但是书上也说,很多时候或者是通常特指 Object Stack,用术语说就是 OGNL value stack。怎么理解 ?
清单 11. 官方的表示
|--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。
清单 12. ActionContext 类
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 对象中。
清单 13. ValueStack 类
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 不要清除值栈,由自己来清除。
参考资料
学习
- “基于 Struts 2 开发 Web 应用”(developerWorks,2009 年 9 月):本文主要介绍在 IBM 产品平台上开发基于 Struts 2 的 Web 应用。Struts 2 与传统的 Struts 1 已经有了本质区别,例如在 Struts 2 中不再需要 ActionForm,任何 Java Bean 都可以用来捕获 form 表单输入参数等。本文将首先介绍如何利用 Rational Software Architect 7(RSA 7)以及 WebSphere Application Server 6.1(WAS 6.1)搭建 Struts 2 开发环境,然后通过一个实例介绍如何开发 Struts 2 应用。
- “使用 Struts 2 进行动态数据处理”(developerWorks,2011 年 4 月):在本文中,将学习 Apache Struts 2 如何帮助您处理基于 Web 的应用程序中的动态数据。将会浏览几个最常见的用例及其代码。探索功能强大的 Action、Interceptor 和 Results 特性如何帮助您用数据库数据填充 UI 字段。还将学习如何在应用程序从值列表加载时通过设置数据值模块化应用程序,以及用所需数据预填字段。
- “Struts 2.0 的 OGNL 组件”(developerWorks,2011 年 4 月):Struts 框架的出现是技术领域的一件革命性事件。许多 Java/J2EE 项目都采用 Struts 框架,该框架逐渐成为所有基于 J2EE 的应用程序中最稳定的框架。尽管只提供控制器支持,但这对于想针对 M (Model) 和 V (View) 区域构建自定义组件的开发人员来说,反而是一种优点。本文主要关注 Struts 2.0 的一个最重要的组件 — Object-Graph Navigation Language (OGNL) — 及其特性。
- “Struts 2 中 JFreeChart 插件的使用分析和功能改进”(developerWorks,2011 年 5 月):Struts 2 支持使用不同的视图技术,其中之一就是直接使用 JFreeChart 报表作为视图技术。Struts 2 通过一种 jfreechart 类型的 Result 来提供对 JFreeChart 的支持,但实际上 Struts 2 的 jfreechart 类型的 Result 要求 Action 返回一个 JFreeChart,这未免太过烦琐。本文将介绍如何在 Struts 2 中使用 JFreeChart 报表,并对 JFreeChart 插件进行改进,使之更方便、易用。
- developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。