使用JSF 2和XSLT导出到Excel

我们使用Java 5,Tomcat 5,Xalan和JSF 1来构build一个使用XSLT,XML和Tomcatfilter的应用程序,以使用户能够以Excel格式导出其数据。 我们最近升级到了Java 1.7.0_07,Tomcat 7.022和JSF 2.1(jsf-api-2.1.0-b03.jar)。 由于涉及的努力,我们还没有升级到facelets; 我们仍然使用jsp的。 我们使用标签在自己的popup窗口中显示Excel报表。 问题是升级后,popup窗口现在在IE中显示原始XML,而不是直接在Excel中popup。 原始的XML可以从浏览器保存到一个文件,如果双击保存的文件,它会在Excel中正确打开,但如果用户可以避免这种解决方法,最好是。

我相信问题在于,JSF 2中的响应现在比在JSF 1中提前。我们的web.xml文件为Tomcat定义了以下filter:

<filter> <filter-name>XSLT Processor</filter-name> <filter-class>com.cs.common.jsf.util.XsltProcessorFilter</filter-class> </filter> <filter-mapping> <filter-name>XSLT Processor</filter-name> <url-pattern>*.xml</url-pattern> </filter-mapping> <filter> <filter-name>Hibernate Session Manager</filter-name> <filter-class>com.cs.common.hibernate.HibernateSessionServletFilter</filter-class> </filter> <filter-mapping> <filter-name>Hibernate Session Manager</filter-name> <url-pattern>*.faces</url-pattern> </filter-mapping> <filter-mapping> <filter-name>Hibernate Session Manager</filter-name> <url-pattern>*.xml</url-pattern> </filter-mapping> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.faces</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.xml</url-pattern> </servlet-mapping> 

我们的XsltProcesserFilter类包含以下几行:

 fChain.doFilter(request, wrapper); response.setContentType("application/vnd.ms-excel"); 

通过使用sysouts ,我确定contentType不是在JSF 2下设置的,可能是因为响应已经被提交。 我已经尝试在输出xml的jsp中设置contentType ,但JSF然后抛出许多错误,所以大概我需要在以后的过程中(如上面的filter)设置它。 我已经尝试过在XsltProcessorFilterdoFilter之前的response.setBufferSize(6400000) ,因为我读过这样做可能会延迟提交,但是这也不能解决问题。

如何在faces完成处理之后但提交之前将contentType设置为application / vnd.ms-excel,以便浏览器打开到Excel?

解决上述问题涉及两个问题。 第一个问题是,Tomcat正在刷新缓冲区并在返回XSLTProcessorFilter之前提交响应。 在将控制权交给Faces之前,通过在XSLTProcessorFilter中将缓冲区大小设置为较大的值来解决这个问题。 接下来,Faces类的JspViewHandlingStrategy在两点刷新输出。 这是通过从XSLTProcessorFilter中添加一个值为“true”的isExcelXML请求属性来克服的。 然后,在JspViewHandlingStrategy中添加编码以检查isXML属性,如果其值为true,则忽略该清除。 在这些更改之后,Excel窗口现在以所需的格式呈现给用户。 当然,我们可以简单地注释掉JspViewHandlingStrategy中的两个刷新,但是大概是为了某种目的,所以这里的解决scheme只是在引起手头问题(当调用XSLTProcessorFilter的时候)才绕过它们。

我们的XSLTProcessorFilter的doFilter方法现在包含修复程序:

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain fChain) throws IOException, ServletException { String xslTemplatePath = request.getParameter(XSLT_REQUEST_PARAM); File xslTemplate = (xslTemplatePath == null) ? null : new File(servletPath, xslTemplatePath); if ((xslTemplatePath != null) && xslTemplate.exists()) { // Run the standard request processing to obtain the XML output CharacterResponseWrapper wrapper = new CharacterResponseWrapper((HttpServletResponse) response); response.setBufferSize(6400000); // This line overcomes Tomcat buffer flushing request.setAttribute("isExcelXML", "true"); // This line signals to JSF to bypass the flushing fChain.doFilter(request, wrapper); response.setContentType("application/vnd.ms-excel"); // Transform the XML using the XSL stylesheet specified on the URL Source xmlSource = new StreamSource(new StringReader(wrapper.toString())); StreamSource xslSource = new StreamSource(xslTemplate); try { Transformer transformer = tFactory.newTransformer(xslSource); StreamResult out = new StreamResult(response.getWriter()); transformer.transform(xmlSource, out); } catch (Throwable t) { t.printStackTrace(response.getWriter()); } } else { // standard processing fChain.doFilter(request, response); } } 

JspViewHandlingStrategy类的改变部分是在它的public void renderView(FacesContext context,UIViewRoot view)方法的末尾:

 //For XML output to Excel, bypass later flushings boolean bypassFlush = false; if (((ServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest()).getAttribute("isExcelXML")!=null && ((ServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest()).getAttribute("isExcelXML").toString().equals("true")) { bypassFlush = true; } // write any AFTER_VIEW_CONTENT to the response (This comment in original JSF file) // side effect: AFTER_VIEW_CONTENT removed (This comment in original JSF file) ViewHandlerResponseWrapper wrapper = (ViewHandlerResponseWrapper) RequestStateManager.remove(context, RequestStateManager.AFTER_VIEW_CONTENT); if (null != wrapper && !bypassFlush) { //fix to Excel issue involved bypassing flush if isExcelXML set to true wrapper.flushToWriter(extContext.getResponseOutputWriter(), extContext.getResponseCharacterEncoding()); } if (!bypassFlush) { //fix to Excel issue involved bypassing flush if isExcelXML set to true extContext.responseFlushBuffer(); } 

由于这需要数周的时间才能解决,所以如果其他人可能会使用所使用的技术,可能需要包含一些关于如何debugging的细节。

debugging始于下载的JSF源代码。 System.out,println()语句放在每个源代码文件中,在其服务方法中以FacesServlet.java开头。 debugging的目标是查看isCommitted布尔值从“false”切换到“true”的位置,因为这是导致问题的原因。 在每个分析的源代码文件中都使用了类似于以下的语句:

 System.out.println("someClass someLineId - " + someObjectReference.getClass().getName() + ((ServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse()).isCommitted() + ((ServletResponse)FacesContext.getCurrentInstance().getExternalContext().getResponse()).getContentType()); 

(请注意,通常必须将“import javax.servlet.ServletResponse;”添加到源文件中的导入语句中。)一旦发现其中的isCommitted值为false且在此之后为true的行,则切换debugging工作到由该行调用的实例化类。 这个过程一直持续到find有问题的缓冲区冲洗线。

当然,刚刚描述的变化必须从项目中运行才能find问题。 每个源文件都必须进行编译(使用包含许多最终项目jar的类path)。 一旦这个类被编译,原来的faces jar被重命名为一个zip文件,并且新编译的类通过覆盖已经存在的那个文件的版本而放入zip文件。 然后,zip文件被重新命名为jar。 然后将jar放入Eclipse中,重新编译项目,并运行该项目。 在Tomcat输出窗口中观察到输出。 当一个编译产生多个类时(就像内部类可能发生的那样),那么所有新编译的类都被放在它们的位置上。 (虽然这可能不是必需的。)