最近产品开发遇到一个比较棘手的问题,我们产品的PDF 生成这快是用Itext 来做的,因为pdf layout比较复杂导致生成pdf的代码写的比较复杂,现在来了一个新的需求,许多新上线的国家想用自己的页面layout跟样式,这就要求我们pdf 生成这块要给每个上线国家做扩展,但是我们没有用pdf template,导致layout的change 涉及到大量的代码要重写,而且每来一个新的国家这部分代码就要重写一下。 从做产品的角度,这样的change 显然是不能接受的,于是就想是不是有跟好的方法可以解决这样的问题,现在网上有很多通过解析html 生成pdf的框架,这就是我要找的方向,经过一段时间研究发现有一个很有用的pdf 生成框架flying saucer ,它不光可以可以解析html/xhtml 生成pdf,就连这些页面使用的css也可以解析并在pdf中反映出来,这不就是我想要的吗?pdf生成逻辑完全被剥离开来,开发者完全不用操心pdf是 怎么生成的,我们只要给每个要生成的pdf做一个html/xhtml的模板并apply相应的样式css(按照business 要求的layout 跟样式) 完全不用写任何的pdf 生成代码,就能够给不同的国家生成不同的pdf.
以下是一段实例代码
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Sample PDF Generation</title> <style type="text/css"> b { color: green; } </style> </head> <body> <p> <b>Greetings Earthlings!</b> We've come for your Java. </p> </body> </html>
看下它的pdf生成代码
package flyingsaucerpdf; import java.io.*; import com.lowagie.text.DocumentException; import org.xhtmlrenderer.pdf.ITextRenderer; public class FirstDoc { public static void main(String[] args) throws IOException, DocumentException { String inputFile = "samples/firstdoc.xhtml"; String url = new File(inputFile).toURI().toURL().toString(); String outputFile = "firstdoc.pdf"; OutputStream os = new FileOutputStream(outputFile); ITextRenderer renderer = new ITextRenderer(); renderer.setDocument(url); renderer.layout(); renderer.createPDF(os); os.close(); } }
我们可以看到生成逻辑非常的简单。 当然在实际使用中我们不会这样做的,我们要生成的pdf template 都不是静态的页面,都需要绑定值的。所以通常的做法是我们会写一个filter 拦截所以的请求,如果请求中有生成pdf的参数我们就把这个请求的response 生成pdf.
比如我们要生成一个report 的pdf 是一个JSF的application,首先我们要做一个resport xhtml的template,生成pdf的请求就相当于对此pdf teamplate页面的访问,当然要pass in 一个参数来告诉我们的filter这个请求返回的结果是否要生成pdf
... <h:dataTable styleClass="companiesTable" value="#{topCompaniesBean.companiesList}" var="company" cellpadding="0" cellspacing="0" headerClass="header" ... rowClasses="odd-row,even-row"> <h:column> <f:facet name="header"> <a href="?Sort=rank">Rank</a> </f:facet> <h:outputText value="#{company.rank}"/> </h:column> <h:column> <f:facet name="header"> <a href="?Sort=name">Name</a> </f:facet> <h:outputText value="#{company.name}"/> </h:column> <h:column> <f:facet name="header"> <a href="?Sort=country">Country</a> </f:facet> <h:outputText value="#{company.country}"/> </h:column> <h:column> <f:facet name="header"> <a href="?Sort=category">Category</a> </f:facet> <h:outputText value="#{company.category}"/> </h:column> <h:column> <f:facet name="header"> <a href="?Sort=sales">Sales</a> </f:facet> <h:outputText value="#{company.sales}"> <f:convertNumber type="currency"/> </h:outputText> </h:column> ... </h:dataTable>
此外,我们要有一个filter,拦截所有请求,如果这个请求是要生成pdf的话,那么它的返回结果将会被flying saucer 解析并生成pdf.
public class RendererFilter implements Filter { ... public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)resp; //Check to see if this filter should apply. String renderType = request.getParameter("RenderOutputType"); if(renderType != null) { //Capture the content for this request ContentCaptureServletResponse capContent = new ContentCaptureServletResponse(response); filterChain.doFilter(request,capContent); ... //Transform the XHTML content to a document //readable by the renderer. StringReader contentReader = new StringReader(capContent.getContent()); InputSource source = new InputSource(contentReader); Document xhtmlContent = documentBuilder.parse(source); ... } ... } ... }
此外为了要去的response 返回的内容,我们要wrapper 一下response
public class ContentCaptureServletResponse extends HttpServletResponseWrapper { private ByteArrayOutputStream contentBuffer; private PrintWriter writer; public ContentCaptureServletResponse(HttpServletResponse resp) { super(resp); } @Override public PrintWriter getWriter() throws IOException { if(writer == null){ contentBuffer = new ByteArrayOutputStream(); writer = new PrintWriter(contentBuffer); } return writer; } public String getContent(){ writer.flush(); String xhtmlContent = new String(contentBuffer.toByteArray()); xhtmlContent = xhtmlContent.replaceAll("<thead>|</thead>|"+ "<tbody>|</tbody>",""); return xhtmlContent; } }
下面的就是生成逻辑了,当然它也是在我们上面写的filter 中的
HttpServletResponse response = (HttpServletResponse)resp; String renderType = request.getParameter("RenderOutputType"); ... StringReader contentReader = new StringReader(capContent.getContent()); InputSource source = new InputSource(contentReader); Document xhtmlContent = documentBuilder.parse(source); ... if(renderType.equals("pdf")){ ITextRenderer renderer = new ITextRenderer(); Renderer.setDocument(xhtmlContent,""); renderer.layout(); response.setContentType("application/pdf"); OutputStream browserStream = response.getOutputStream(); renderer.createPDF(browserStream); return; }
大家可以看到,如果request 中的renderType等于pdf的话,我们就会调用itext跟flying saucer 的API把response 生成一个pdf.
大家可以看到上面这段生成pdf的逻辑非常简单, 是真正的write once , apply every where。 来再多的需求,我们只要create 相应的template(with CSS) 其余的生成逻辑全都一样。
希望这篇文章对跟我遇到相同问题的人有所帮助。