本博客是自己在学习和工作途中的积累与总结,仅供自己参考,也欢迎大家转载,转载时请注明出处,请尊重他人努力成果,谢谢。
前提准备:
1. 项目中至少需要引入的jar包,注意版本:
a) core-renderer.jar
b) freemarker-2.3.16.jar
c) iText-2.0.8.jar
d) iTextAsian.jar
上代码:
注释: 此类为自定义的Tag类的基类,在action中怎么放的数据,在ftl中就怎么取数据,简洁明了。
1. 自定义Tag类的基类
/**
* 通用的生成pdf预览和生成打印的html文件
*
* @author xg君
*
*/
public abstract class PDFTag extends BodyTagSupport {
private static final long serialVersionUID = 1L;
// 标签属性变量
private String json = "";
private String tempDir = "";
// 非标签属性变量
private Map<String, Object> rootMap = new HashMap<String, Object>();
private String templateStr = null;
private String freemarkereConfigurationBeanName = null;
private String fileName = null;
private String basePath = null;
private String fileEncoding = "utf-8";
@Override
public int doStartTag() throws JspException {
setConfigParams();
WebApplicationContext application = WebApplicationContextUtils.getWebApplicationContext(pageContext
.getServletContext());
Map<String, Object> map = parseJSON2Map(json);
doServiceStart();
rootMap.putAll(map);
if (freemarkereConfigurationBeanName == null) {
try {
throw new CstuException("FreemarkereConfigurationBeanName不能为空!");
} catch (CstuException e) {
e.printStackTrace();
}
}
Configuration cptFreemarkereConfiguration = (Configuration) application
.getBean(freemarkereConfigurationBeanName);
try {
if (templateStr == null) {
throw new CstuException("模板文件不能为空!");
}
Template template = cptFreemarkereConfiguration.getTemplate(templateStr);
if (basePath == null) {
throw new CstuException("文件的基本路径(父路径)不能为空!");
}
File htmlPath = new File(tempDir + File.separator + basePath);
if (!htmlPath.exists()) {
htmlPath.mkdirs();
}
if (fileName == null) {
throw new CstuException("生成的html文件名不能为空!");
}
File htmlFile = new File(htmlPath, File.separator + fileName);
if (!htmlFile.exists()) {
htmlFile.createNewFile();
}
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(htmlFile), fileEncoding));
template.process(rootMap, out);
out.flush();
doServiceDoing();
// 显示在页面
template.process(rootMap, pageContext.getResponse().getWriter());
} catch (Exception e) {
e.printStackTrace();
}
doServiceEnd();
return SKIP_BODY;
}
/**
* 启用配置的参数
*/
public abstract void setConfigParams();
/**
* 业务处理方法-开始 填充数据
*
* @return
*/
public abstract void doServiceStart();
/**
* 业务处理方法-执行中 备用,可空实现,若rootMap中存在双份数据则可在此处填充判断条件
*
* @return
*/
public abstract void doServiceDoing();
/**
* 业务处理方法-结束 清空rootMap并调用垃圾回收,也可空实现
*
* @return
*/
public abstract void doServiceEnd();
/**
* 将元素放入rootMap中
*/
public void putKV(String key, Object value) {
rootMap.put(key, value);
}
/**
* 将map放入rootMap中
*
* @param m
*/
public void putMap(Map m) {
rootMap.putAll(m);
}
public void clear() {
rootMap.clear();
rootMap = null;
}
/**
* 移除元素
*
* @param key
* @return
*/
public Object remove(String key) {
return rootMap.remove(key);
}
public static Map<String, Object> parseJSON2Map(String jsonStr) {
Map<String, Object> map = new HashMap<String, Object>();
JSONObject json = JSONObject.fromObject(jsonStr);
for (Object k : json.keySet()) {
Object v = json.get(k);
if (v instanceof JSONArray) {
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
Iterator<JSONObject> it = ((JSONArray) v).iterator();
while (it.hasNext()) {
JSONObject json2 = it.next();
list.add(parseJSON2Map(json2.toString()));
}
map.put(k.toString(), list);
} else {
map.put(k.toString(), v);
}
}
return map;
}
public String getJson() {
return json;
}
public void setJson(String json) {
this.json = json;
}
public String getTempDir() {
return tempDir;
}
public void setTempDir(String tempDir) {
this.tempDir = tempDir;
}
public String getTemplateStr() {
return templateStr;
}
public void setTemplateStr(String templateStr) {
this.templateStr = templateStr;
}
public String getFreemarkereConfigurationBeanName() {
return freemarkereConfigurationBeanName;
}
public void setFreemarkereConfigurationBeanName(String freemarkereConfigurationBeanName) {
this.freemarkereConfigurationBeanName = freemarkereConfigurationBeanName;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
public String getFileEncoding() {
return fileEncoding;
}
public void setFileEncoding(String fileEncoding) {
this.fileEncoding = fileEncoding;
}
}
注释: setConfigParams方法是用于调用接口定义的配置参数的方法,如:templateStr,basePath等,doServiceStart,doServiceDoing和doServiceEnd等方法用于处理业务逻辑,比如我的需求是做出合同在一个页面显示,要分页,要加水印,但生成的pdf样式与预览的是不同的,所以我加了一个doServiceDoing中给rootMap添加判断条件,这样就能一个flt文件作出两种效果(预览和打印),当然如果预览和打印要一样的效果,doServiceDoing方法可以空实现。这四个方法总结一下就是:
1. setConfigParams : 配置参数
2. doServiceStart : 填充数据/条件
3. doServiceDoing : 填充数据/条件,到这里是个分界线,此方法之前,rootMap数据先进入html再进入浏览器(预览),此方法之后,rootMap数据会再次进入html文件,结束,所以此处可写判断
4. doServiceEnd : 可有可无,我还是写上了,万一哪天的数据集太大,此处便可以在数据填充完后,清理掉,节省内存空间
2. PDFTag的子类
/**
* 用户自定义PDFTag类
*
* @author xg君
*
*/
public class ViewPDFTag extends PDFTag {
private static final long serialVersionUID = 4528567203497016087L;
private String prjNature = "";
private String bookName = "";
private String prjCode = "";
/**
* 用户自定义的配置参数
*/
public PDFConfigurationInterface pDFConfigurationInterface = new PDFConfigurationInterface() {
@Override
public void configTemplateStr() {
// 自定义的业务参数
if (prjNature.equalsIgnoreCase("2") || prjNature.equalsIgnoreCase("1")) {
setTemplateStr("wj-project-print.ftl");
}
}
@Override
public void configFreemarkereConfigurationBeanName() {
setFreemarkereConfigurationBeanName("cptFreemarkereConfiguration");
}
@Override
public void configFileName() {
// 填入html文件
setFileName(prjCode + ".html");
}
@Override
public void configFileEncoding() {
// 默认utf-8
}
@Override
public void configBasePath() {
setBasePath("html_pdf");
}
};
@Override
public void doServiceStart() {
putKV("prjNature", prjNature);
putKV("bookName", bookName);
putKV("flag", "0");
}
@Override
public void doServiceDoing() {
putKV("flag", "1");
}
@Override
public void doServiceEnd() {
clear();
System.gc();
}
public String getPrjNature() {
return prjNature;
}
public void setPrjNature(String prjNature) {
this.prjNature = prjNature;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public String getPrjCode() {
return prjCode;
}
public void setPrjCode(String prjCode) {
this.prjCode = prjCode;
}
@Override
public void setConfigParams() {
pDFConfigurationInterface.configTemplateStr();
pDFConfigurationInterface.configFreemarkereConfigurationBeanName();
pDFConfigurationInterface.configFileName();
pDFConfigurationInterface.configBasePath();
pDFConfigurationInterface.configFileEncoding();
}
}
注释: 1. PDFConfigurationInterface 是自定义的标签参数配置接口,子类必须定义一个该接口的实现类的成员变量或定义一个成员变量内部类,并在setConfigParams方法中调用使其生效。
2. 其实这个setConfigParams方法可以不用写,的用反射就能在后面全部自动执行,但是考虑到有些参数是默认的,可以不用设置,所以还写了setConfigParams方法
3. 子类的成员变量接收在tld文件中配置的属性。
3. 自定义的标签参数配置接口
/**
* PdfTag类的配置
*
* @author xg君
*
*/
public interface PDFConfigurationInterface {
/**
* 设置模板名称
*/
void configTemplateStr();
/**
* 设置配置的FreemarkereConfigurationBean的名称
*/
void configFreemarkereConfigurationBeanName();
/**
* 设置生成的html文件名称
*/
void configFileName();
/**
* 设置生成的html文件的基本路径(父目录)
*/
void configBasePath();
/**
* 设置文件编码,默认utf-8
*/
void configFileEncoding();
}
4. 自定义异常类
/**
* 自定义异常类
*
* @author Administrator
*
*/
public class CstuException extends Exception {
private static final long serialVersionUID = 4266461814747405870L;
public CstuException(String msg) {
super(msg);
}
}
5. tld文件配置
<tag>
<name>print</name>
<tagclass>com.iris.taglib.web.PreviewPDFTag</tagclass>
<bodycontent>JSP</bodycontent>
<attribute>
<name>json</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>prjNature</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>bookName</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>tempDir</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>prjCode</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
6. action( 在action中,存在request域中的数据是怎么放入的,那么在*.ftl文件中就怎么取)
/**
* 处理PDF导出
*
*/
@Namespace("/export")
@Results({ @Result(name = "exceltemplate", location = "/WEB-INF/content/pdf/export-pdf.jsp"),
@Result(name = "exprotPdf2", location = "/WEB-INF/content/project/project/export/export-pdf2.jsp") })
public class ExportPdfAction extends ActionSupport {
private static final long serialVersionUID = -5454188364706173477L;
@Value("${tempDir}")
private String tempDir;
@Value("${pdfFont}")
private String pdfFont;
@Value("${staticResRootDir}")
private String staticResRootDir;
@Value("${staticResRootDir2}")
private String staticResRootDir2;
@Value("${WaterMarkImgDir}")
private String waterMarkImgDir;
@Autowired
private ProjectService projectService;
@Autowired
private PersonService personService;
@Autowired
private ConstDictionaryService constDictionaryService;
@Autowired
private FdPlanDetailService fdPlanDetailService;
@Autowired
private ServiceFactory serviceFactory;
@Action("exprotPdf2")
public String exprotPdf2() {
String prjCode = Struts2Utils.getParameter("prjCode");
prjCode = Struts2Utils.decodeDesString(prjCode);
Project project = projectService.getProjectById(Long.parseLong(prjCode));
Map<String, String> baseInfo = new HashMap<String, String>();
// 此处将数据放入baseInfo 的map中。并返回给页面
return "exprotPdf2";
}
public List<Map<String, String>> getMembers(String xmlData) throws Exception {
return list;
}
/**
* 为字符串添加指定字符
*
* @param num
* @param splitStr
* @param str
* @return
*/
public String addStr(int num, String splitStr, String str) {
StringBuffer sb = new StringBuffer();
String temp = str;
int len = str.length();
while (len > 0) {
if (len < num) {
num = len;
}
sb.append(temp.substring(0, num)).append(splitStr);
temp = temp.substring(num);
len = temp.length();
}
return sb.toString();
}
/**
* 两个数字/英文
*
* @param str
* @param num
* @return 最终索引
*/
public int getEndIndex(String str, double num) {
int idx = 0;
int count = 0;
double val = 0.00;
// 判断是否是英文/数字
for (int i = 0; i < str.length(); i++) {
if ((str.charAt(i) >= 'A' && str.charAt(i) <= 'Z') || (str.charAt(i) >= 'a' && str.charAt(i) <= 'z')
|| Character.isDigit(str.charAt(i))) {
val += 0.50;
} else {
val += 1.00;
}
count = i + 1;
if (val >= num) {
idx = i;
break;
}
}
if (idx == 0) {
idx = count;
}
return idx;
}
/**
* 下载pdf文件
*
* @return
*/
@Action("downLoad")
public String downLoad() {
String prjCode = Struts2Utils.getParameter("id");
String basePath = "html_pdf";
Project project = projectService.getProjectById(prjCode);
String zhTitle = project.getZhTitle();
FileOutputStream fos = null;
// html
// 通过自定义标签生成的html转成pdf
......
// 添加水印
// 拿到pdf
File pdfFile = new File(wm_pdf);
BufferedOutputStream out = null;
FileInputStream in = null;
try {
in = new FileInputStream(pdfFile);
HttpServletResponse response = Struts2Utils.getResponse();
response.reset();
String fileName = zhTitle + ".pdf";
String fileName2 = URLEncoder.encode(fileName, "UTF-8");
String agent = Struts2Utils.getRequest().getHeader("USER-AGENT");
// IE
if (null != agent && -1 != agent.indexOf("MSIE")) {
fileName2 = new String(fileName.getBytes("GBK"), "ISO8859-1");
} else if (null != agent && -1 != agent.indexOf("Mozilla")) {
fileName2 = new String(fileName.getBytes("UTF-8"), "ISO8859-1");
}
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName2 + "\"");
response.setContentType(FileContentTypes.getContentType(zhTitle + ".pdf"));
out = new BufferedOutputStream(response.getOutputStream());
byte[] buffer = new byte[16 * 1024];
int len = 0;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 清理残留文件
// if (htmlFile.exists()) {
// htmlFile.delete();
// }
File temp_file = new File(pdfPath);
if (temp_file.exists()) {
temp_file.delete();
}
if (pdfFile.exists()) {
pdfFile.delete();
}
return null;
}
public BigDecimal checkNumber(String number) {
// 初始化为6位小数
DecimalFormat df = new DecimalFormat("0.000000");
String num = df.format(Double.parseDouble(number));
BigDecimal bd = new BigDecimal(num);
String val = bd.toString();
val = val.replaceAll("^(0+)", "");
val = val.replaceAll("(0+)$", "");
int idx = val.indexOf(".");
int len = val.substring(idx + 1).length();
if (len < 2) {
if (len == 0 && idx == 0) {
bd = new BigDecimal("0.00");
} else {
bd = new BigDecimal(val).setScale(2);
}
} else {
bd = new BigDecimal(val).setScale(len);
}
return bd;
}
private String replaceStr(String str, String reVal) {
Pattern pattern = Pattern.compile("^" + reVal + "+|" + reVal + "+$");
Matcher matcher = pattern.matcher(str);
return matcher.replaceAll("");
}
/**
* 添加水印
*/
private void waterMark(BufferedOutputStream bos, String input, String waterMarkName, String imagePath) {
try {
PdfReader reader = new PdfReader(input);
PdfStamper stamper = new PdfStamper(reader, bos);
int total = reader.getNumberOfPages() + 1;
PdfContentByte content;
BaseFont base = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
PdfGState gs = new PdfGState();
for (int i = 1; i < total; i++) {
content = stamper.getOverContent(i);// 在内容上方加水印
content.setGState(gs);
content.beginText();
Image image = Image.getInstance(imagePath);
image.setAbsolutePosition(-30, 200);
image.scalePercent(80);
content.addImage(image);
content.endText();
}
stamper.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
7. ftl文件(模板文件)
补充:ftl文件是通过取得标签传入的数据,通过freemarker表达式进行取值操作,关于freemarker表达式的使用,后期将抽空补上
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>打印预览</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
</head>
<#if "${flag}" = "0">
<body style="background:#ffffff;font-size:10px;">
</#if>
<#if "${flag}" = "1">
<body>
</#if>
<#if "${flag}" = "1">
<div class="top">
<!-- <a id="printbt" href="#" class="grey_but mw50" style="float: right;margin-right: 10px;margin-top: 2px;">打印</a> -->
<a id="importbt" href="#" class="grey_but mw50" style="float: right;margin-right: 10px;margin-top: 2px;">导出pdf</a>
</div>
</#if>
<#if "${flag}" = "1">
<div style="width: 1000px;height: 50px;margin-left: auto;margin-right:auto;"></div>
</#if>
<div id="p_content" class="print_content">
<#if "${flag}" = "0">
<div class="table_block" style="width: 680px;margin-top:0px">
<#else>
<div class="table_block" style="width: 1000px;margin-top:0px">
</#if>
<img style="width: 900px;height: 900px;position: absolute;top:-100px" src="${baseInfo.waterMarkImg}" />
<table width="90%" border="0" cellspacing="0" cellpadding="0" style="margin-left: auto;margin-right:auto;border-color: #000000;border-collapse: collapse;table-layout:fixed;">
<tr>
<td colspan="5" style="border-bottom:none;border-top: none;border-left: none;border-right: none;" class="title">${bookName}</td>
</tr>
<tr class="text">
<td>xxxx</td>
<td>${xxxx.xxxx}</td>
<#if "${xxxx.xxxx}" = "1">
<td>xxxx/xxxx</td>
<td colspan="2">${xxxx.xxxx}/${xxxx.xxxx!''}</td>
</#if>
<#if "${xxxx.xxxx}" = "2">
<td>xxxx</td>
<td colspan="2">${xxxx.xxxx}/${xxxx.xxxx}</td>
</#if>
</tr>
<tr class="text">
<td>xxxx</td>
<td>${xxxx.xxxx}</td>
<td>xxxx</td>
<td colspan="2">${xxxx.xxxx}</td>
</tr>
<tr class="text">
<td>xxxx</td>
<td>${xxxx.xxxx}</td>
<td>xxxx</td>
<td colspan="2">${xxxx.xxxx}</td>
</tr>
<tr class="text">
<td>xxxx</td>
<td colspan="4">${xxxx}</td>
</tr>
<tr>
<td class="middle" colspan="5">xxxx</td>
</tr>
<#if "${xxxx.xxxx}" = "1">
<tr class="text">
<td></td>
<td>xxxx</td>
<td>xxxx</td>
<td>xxxx</td>
<td>xxxx</td>
</tr>
<#list xxxxas xxxx>
<tr class="text">
<td>${xxxx.xxxx}</td>
<td>${xxxx.xxxx_p}</td>
<td>${xxxx.xxxx}</td>
<td>${xxxx}</td>
<td>${xxxx}</td>
</tr>
</#list>
</#if>
<#if "${xxxx}" = "2">
<tr class="text">
<td>xxxx</td>
<td>xxxx</td>
<td>xxxx</td>
<td>xxxx</td>
<td>xxxx</td>
</tr>
<#list projectMember as member>
<tr class="text">
<td>${xxxx.xxxx_p}</td>
<td>${xxxx.xxxx}</td>
<td>${xxxx.xxxx}</td>
<td>${xxxx.xxxx}</td>
<td>${xxxx.xxxx}</td>
</tr>
</#list>
</#if>
<tr>
<td colspan="5" style="padding: 5px;border-bottom: none;">
<div>xxxx:</div>
<div>1.xxxx;</div>
<div>2.xxxx;</div>
<div>3.xxxx;</div>
<div>4.xxxx;</div>
<div>5.xxxx。</div>
</td>
</tr>
<tr style="height:180px;padding:0px">
<td colspan="5" style="border:none ;margin:0px;padding:0px;height:180px;">
<table width="100%" border="0" style="border-collapse: collapse;table-layout:fixed; <#if "${flag}" = "0">word-break:break-strict;</#if>height:180px;">
<tr>
<td style="padding: 0px;">
<table class="innerTB" width="100%" border="0" style="border-collapse: collapse;table-layout:fixed; <#if "${flag}" = "0">word-break:break-strict;</#if>">
<tr>
<td>xxxx(xxxx):</td>
</tr>
<tr>
<td> </td>
</tr>
<tr>
<td> </td>
</tr>
<tr>
<td style="text-align: right;">年 月 日</td>
</tr>
<tr>
<td> </td>
</tr>
</table>
</td>
<td style="padding: 0px;">
<table class="innerTB" width="100%" border="0" style="border-collapse: collapse;table-layout:fixed; <#if "${flag}" = "0">word-break:break-strict;</#if>">
<tr>
<td>xxxx(xxxx)</td>
</tr>
<tr>
<td>xxxx:</td>
</tr>
<tr>
<td> </td>
</tr>
<tr>
<td>(xxxx)</td>
</tr>
<tr>
<td style="text-align: right;">年 月 日</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
预览效果:
打印效果:
打印的话仅需在预览页面添加一段js代码:
window.print();这是打印整个页面
当然也可以打印指定区域,需要用到css3的@media print
@media print {
.noprint {
display: none
}
}
在不打印区域添加<div class="noprint"></div>即可,主要是noprint样式.
2017-07-11 更新补充bug或局限性:
(1). 项目后期需求更改将放入各种类型参数,因此在后台将数据转成json,将会比较花时间,导致客户端响应时间延长
解决方案1(推荐): 在自定义标签中,通过pageContext内置对象,获取Request实例,map = (Map<String, Object>) pageContext.getRequest().getAttribute(dataKey);即可获取数据对象,然后放入rootMap中,共、供ftl标签使用,即后台正常将数据放入request域且不用转换成json对象
解决方案2(不推荐): 采用jackson工具,将对象转成json字符串,仅能解决数据转换问题,并不能减少客户端响应时间
(2) 补充,以下jar包 同样能够生产PDF
1) flying-saucer-pdf-text5-9.0.2.jar
2) flying-saucer-core-9.0.2.jar
3) flying-saucer-log4j-9.0.2.jar
4) itextpdf-5.3.4iris.jar