特别提醒:下面的分析是以struts2中的freemarker引擎进行分析
在使用struts2的UI组件的时候为程序的界面编写带来了很大的便利,它可以使程序员少写很多重复的代码,而是将那些得复的界面逻辑抽象到模板中进行封装,除了少写很多重复的代码,同时可以使用整个界面拥有一致的观感,确实是一个种解决界面编程的好方案.这以我在实际开发中遇到的一个问题,而引发我对组件之间数据通信的一点点思考,至于真实的情况是不是这样还需要日后的验证.
在实际开发中我希望可以设计一套支持多列表单定制的主题,这涉及到一个变量共享的问题,即我想在表单的最外层存放一些变量:类似表单的列数,或表单某些默认属性等,这个存在地点最佳的地方是form组件之中,退而求其次可以使用<@s.push />或<@s.set />组件.至于如何的引用这些变量,以及存在的问题,首先需要我先弄清一个事实,那就是当两个组件之间存在包含关系,它们的执行逻辑是怎样的.
从模板编写来说,我们在模板中引用变量的时候,整个Component的变量我们都是可以访问的,我们知道模板中的上下文,实际与值栈中的上下文进行了统一处理,也就是说在模板在被解析的时候UI组件是位于值栈中的,那么当UI组件的子组件在进行解析的时候当前UI组件对象是否还位于值栈中呢?答案是否定的.这与模板的处理机制有关,要弄清这个问题先需要关注UI组件对象是何时进入值栈,而又是何是退出值栈的,通过分析源代码你会发现,每当进行一个模板合并(mergeTemplate)操作都会有一次UI组件的入栈及相应的出栈操作,此合并操作被放在UIBean基类对象中,基关键代码如下:
protected void mergeTemplate(Writer writer, Template template) throws Exception { final TemplateEngine engine = templateEngineManager.getTemplateEngine(template, templateSuffix); if (engine == null) { throw new ConfigurationException("Unable to find a TemplateEngine for template " + template); } if (LOG.isDebugEnabled()) { LOG.debug("Rendering template " + template); } final TemplateRenderingContext context = new TemplateRenderingContext(template, writer, getStack(), getParameters(), this); engine.renderTemplate(context); }
从上面的代码你还不能看出什么问题,实际入栈出栈的逻辑是TemplateEngine#renderTemplate方法中的基代码如下:
public void renderTemplate(TemplateRenderingContext templateContext) throws Exception { // get the various items required from the stack ValueStack stack = templateContext.getStack(); Map context = stack.getContext(); ServletContext servletContext = (ServletContext) context.get(ServletActionContext.SERVLET_CONTEXT); HttpServletRequest req = (HttpServletRequest) context.get(ServletActionContext.HTTP_REQUEST); HttpServletResponse res = (HttpServletResponse) context.get(ServletActionContext.HTTP_RESPONSE); // prepare freemarker Configuration config = freemarkerManager.getConfiguration(servletContext); // get the list of templates we can use List<Template> templates = templateContext.getTemplate().getPossibleTemplates(this); // find the right template freemarker.template.Template template = null; String templateName = null; Exception exception = null; for (Template t : templates) { templateName = getFinalTemplateName(t); try { // try to load, and if it works, stop at the first one template = config.getTemplate(templateName); break; } catch (ParseException e) { // template was found but was invalid - always report this. exception = e; break; } catch (IOException e) { // FileNotFoundException is anticipated - report the first IOException if no template found if (exception == null) { exception = e; } } } if (template == null) { if (LOG.isErrorEnabled()) { LOG.error("Could not load the FreeMarker template named '" + templateContext.getTemplate().getName() +"':"); for (Template t : templates) { LOG.error("Attempted: " + getFinalTemplateName(t)); } LOG.error("The TemplateLoader provided by the FreeMarker Configuration was a: "+config.getTemplateLoader().getClass().getName()); } if (exception != null) { throw exception; } else { return; } } if (LOG.isDebugEnabled()) { LOG.debug("Rendering template " + templateName); } ActionInvocation ai = ActionContext.getContext().getActionInvocation(); Object action = (ai == null) ? null : ai.getAction(); SimpleHash model = freemarkerManager.buildTemplateModel(stack, action, servletContext, req, res, config.getObjectWrapper()); model.put("tag", templateContext.getTag()); model.put("themeProperties", getThemeProps(templateContext.getTemplate())); // the BodyContent JSP writer doesn't like it when FM flushes automatically -- // so let's just not do it (it will be flushed eventually anyway) Writer writer = templateContext.getWriter(); final Writer wrapped = writer; writer = new Writer() { public void write(char cbuf[], int off, int len) throws IOException { wrapped.write(cbuf, off, len); } public void flush() throws IOException { // nothing! } public void close() throws IOException { wrapped.close(); } }; try { stack.push(templateContext.getTag()); template.process(model, writer); } finally { stack.pop(); } }
从最后几行代码你可以清楚的看到UI组件先入栈,之后执行模板的解析操作,最后UI组件马上就出栈了,通常一个UI组件的合并操作会在两个地方被执行,标签的开始处,与标签的结束时候,这两个控制逻辑方法分别在UIBean与CloseingUIBean中定义,这两个周期方法的代码如下:
public boolean end(Writer writer, String body) { evaluateParams(); try { super.end(writer, body, false); mergeTemplate(writer, buildTemplateName(template, getDefaultTemplate())); } catch (Exception e) { throw new StrutsException(e); } finally { popComponentStack(); } return false; }
上面的end方法是在UIBean中定义的,下面的start方法是在CloseingUIBean中定义的
public boolean start(Writer writer) { boolean result = super.start(writer); try { evaluateParams(); mergeTemplate(writer, buildTemplateName(openTemplate, getDefaultOpenTemplate())); } catch (Exception e) { LOG.error("Could not open template", e); e.printStackTrace(); } return result; }
从上面分析基本可以解释子组件一般是不可能通过值栈去访问父组件的,因为一个组件都是快入快出,只是在合并模板的时候才会有此操作,既然当一个子组件正在被解析处理的时候其它组件已经不在栈中那么组件如果要进行数据交换又当如何处理,或者说struts2本身就不支持组件之间数据交换,关于这一点答案是否定的,数据交换是可以进行的,其实关于这一点有一个这样的事实:就是关于theme变量的问题,如果一个UI组件指定的theme属性则会使用自己定义的;如果自己没有定义则使用form组件定义的theme组件定义,如果form组件也没有定义则使用,page,request,session,application定义的,如果都没有定义则使用默认的.从这样一个事实我们可以看到一个UI组件它们可以获取form的theme参数,那这些组件它们是如何做到的呢?答案也是在UIBean中:
public String getTheme() { String theme = null; if (this.theme != null) { theme = findString(this.theme); } if ( theme == null || theme.equals("") ) { Form form = (Form) findAncestor(Form.class); if (form != null) { theme = form.getTheme(); } } // If theme set is not explicitly given, // try to find attribute which states the theme set to use if ((theme == null) || (theme.equals(""))) { theme = stack.findString("#attr.theme"); } // Default theme set if ((theme == null) || (theme.equals(""))) { theme = defaultUITheme; } return theme; }
从上面的方法的我们可以看到有一个关键的方法findAncestor从字面意思上看是查找祖先,此方法的源代码如下:
protected Component findAncestor(Class clazz) { Stack componentStack = getComponentStack(); int currPosition = componentStack.search(this); if (currPosition >= 0) { int start = componentStack.size() - currPosition - 1; //for (int i = componentStack.size() - 2; i >= 0; i--) { for (int i = start; i >=0; i--) { Component component = (Component) componentStack.get(i); if (clazz.isAssignableFrom(component.getClass()) && component != this) { return component; } } } return null; }
通过源代码的分析你会发现对于UI组件的处理,框架原专门为它们维系了一个栈,来表示它们的先后关系:
public Stack getComponentStack() { Stack componentStack = (Stack) stack.getContext().get(COMPONENT_STACK);//其值为__component_stack if (componentStack == null) { componentStack = new Stack(); stack.getContext().put(COMPONENT_STACK, componentStack); } return componentStack; }
有了这样一个UI组件的独立的栈之后,开发人员可以通过此栈在组件之间进行数据交换,当一个组件被构建时此组件为进行进入到这个特别的栈中,这个栈并不是值栈,而是一个普通的Stack对象:具体查看Component类的构造方法:
public Component(ValueStack stack) { this.stack = stack; this.parameters = new LinkedHashMap(); //构造之后组件自己就被放入了栈当中去了 getComponentStack().push(this); }
那么此组件对象又是何时出栈的呢?就在前面展示的end方法中,即当一个标签结束的时候此组件对象才会出栈,此组件中的所有子组件都可以通过此栈来访问当前组件对象,其实在struts2内部有一大批组件之间需要进行信息的交换例如OptGroup,UpDownSelect,TreeNode,Param等等都需要例如Param标签它通常都位一个组件内部为它的父组件传递参数,其end源代码如下:
public boolean end(Writer writer, String body) { Component component = findAncestor(Component.class); if (value != null) { if (component instanceof UnnamedParametric) { ((UnnamedParametric) component).addParameter(findValue(value)); } else { String name = findString(this.name); if (name == null) { throw new StrutsException("No name found for following expression: " + this.name); } Object value = findValue(this.value); component.addParameter(name, value); } } else { if (component instanceof UnnamedParametric) { ((UnnamedParametric) component).addParameter(body); } else { component.addParameter(findString(name), body); } } return super.end(writer, ""); }
对于特别组件form还有一个特别的处理,在UIBean组件的evaluateParams中有段代码如下:
final Form form = (Form) findAncestor(Form.class); // create HTML id element populateComponentHtmlId(form); if (form != null ) { addParameter("form", form.getParameters()); if ( name != null ) { // list should have been created by the form component List tags = (List) form.getParameters().get("tagNames"); tags.add(name); } }
可以看到任何一个UI组件都会有一个form参数它会存放它们的父组件form,当然前提是它存在话,这样就解决定我先前提出的问题,将全局信息存储在form表单组件中,实际使用
<@s.push />或<@s.set />组件也可以进行数据交换,只需要在模板中直接进行值栈的引用这些变量就可以了