史上最全的springboot导出pdf⽂件
  最近项⽬有⼀个导出报表⽂件的需求,我脑中闪过第⼀念头就是导出pdf(产品经理没有硬性规定导出excel还是pdf⽂件),于是赶紧上⽹查看相关的资料,直到踩了⽆数的坑把功能做出来了才知道其实导出excel的api更⽅便,⽹上的相关博客也更多,不过坑也踩完了,这⼀次就来把代码和收获整理和分享⼀下。
导出pdf模板的局限性
  在看到需求之前,我先去⽹上去搜了⼀波,发现⽹上很⼤⼀部分博客都是将如何导出pdf模板的,就是先制作⼀张pdf模板,把固定不变的地⽅先写好,把需要改变的地⽅留⽩并设置参数,然后在代码⾥为参数赋值就⾏了,这种⽅式很简单,代码量也很少,但是!!!这种⽅式只适合导出格式和内容是固定的⽂件,⽽我们的需求是导出⼀张以产品名称为列,产品属性为⾏的报表,产品数量不定,这很显然就不能⽤模板的⽅式,只好⽼⽼实实把报表的数据从头到尾⼀⼀导出来。
使⽤iText导出pdf表格
  iText是⼀种⽣成PDF报表的Java组件,先把jar包下下来,maven依赖如下:
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.0.6</version>
</dependency>
按照惯例,先来⽣⼀个“Hello World”的⽂件,代码如下
public class TestPdf {
public static void main(String[] args) throws Exception {
TestPdf pdf = new TestPdf();
String filename = "D:/Program Files/pdfTest/testTable3.pdf";
System.out.println("打印完成");
}
public void createPDF(String filename) throws IOException {
Document document = new Document(PageSize.A4);
try {
document.addTitle("example of PDF");
document.open();
document.add(new Paragraph("Hello World!"));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
} finally {
document.close();
}
}
}
这个没什么可说的,照着写就⾏了,不过我们导出的pdf⼤多数是以表格的形式,所以我就直接切⼊主题了,这⾥要⽤到⼀个很关键的类pdf.PDFPTable,先导出⼀张两⾏两列的表格public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
PdfPTable table = new PdfPTable(2);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 15;
cell = new PdfPCell(new Phrase("one"));
cell.setFixedHeight(size);//设置⾼度
table.addCell(cell);
cell = new PdfPCell(new Phrase("two"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("three"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("four"));
cell.setFixedHeight(size);
table.addCell(cell);
return table;
}
public void createPDF(String filename) throws IOException {
Document document = new Document(PageSize.A4);
try {
PdfWriter writer = Instance(document, new FileOutputStream(filename));
document.addTitle("example of PDF");
document.open();
PdfPTable table = createTable(writer);
document.add(table);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
} finally {
document.close();
}
}
效果如下图
现在把我们的需求变得更苛刻⼀点,再增加⼀⾏,格⼦数为1,⽂字⽔平和垂直居中对齐,占两⾏⾼度,先贴代码再做解释
public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
PdfPTable table = new PdfPTable(2);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 15;
cell = new PdfPCell(new Phrase("one"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("two"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("three"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("four"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("five"));
cell.setColspan(2);//设置所占列数
cell.setFixedHeight(size*2);//设置⾼度
cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置⽔平居中
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
table.addCell(cell);
return table;
}
这⾥有⼀个很坑的地⽅,就是每⼀⾏的长度要和初始化表格的长度相等,即要把整⾏给占满,否则后⾯的都不会打印出来,所以要⽤到setColspan()⽅法来合并列,接下来还有⼀个就是合并⾏的的⽅法setRowspan()。我们在导出pdf报表的实际操作中经常会有这样的需求
所以合并⾏的⽅法显得尤为重要,那就尝试⼀下⽤这个⽅法来合并⾏,
public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
PdfPTable table = new PdfPTable(2);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 20;
cell = new PdfPCell(new Phrase("one"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("two"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("three"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("four"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("five"));
cell.setColspan(1);//设置所占列数
cell.setRowspan(2);//合并⾏
cell.setFixedHeight(size*2);//设置⾼度
cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置⽔平居中
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
table.addCell(cell);
cell = new PdfPCell(new Phrase("six"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("seven"));
cell.setFixedHeight(size);
table.addCell(cell);
return table;
}
效果如下
事实上很简单,在原来的代码上将“five”这⼀⾏长度由2变为1,加上setRowspan(2)⽅法,再加上两⾏长度为1的cell就是上⾯的效果了。看起来也没有多复杂是吧,现在我们在jar包上⽤了较新的版本itextpdf-5.0.6.jar,然⽽在这之前还有⼀种⽼版本的jar包itext-2.1.7.jar,依赖如下:
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
⽤了这个jar包后发现效果是⼀样的,那我为什么要提这个版本呢,在后⾯我们会讲打到。事实上,在实现⾏合并的还有⼀种⽅法就是table中再套⼀个table,代码如下:
public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
PdfPTable table = new PdfPTable(2);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 20;
cell = new PdfPCell(new Phrase("one"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("two"));
cell.setFixedHeight(size);
cell.setColspan(2);
table.addCell(cell);
cell = new PdfPCell(new Phrase("three"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("four"));
cell.setFixedHeight(size);
cell.setColspan(2);
table.addCell(cell);
cell = new PdfPCell(new Phrase("five"));
cell.setColspan(1);//设置所占列数
//cell.setRowspan(2);//合并⾏
cell.setFixedHeight(size*2);//设置⾼度
cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置⽔平居中
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
table.addCell(cell);
PdfPTable table2 = new PdfPTable(1);//新建⼀个table
cell = new PdfPCell(new Phrase("six"));
cell.setFixedHeight(size);
table2.addCell(cell);
cell = new PdfPCell(new Phrase("seven"));
cell.setFixedHeight(size);
table2.addCell(cell);
cell = new PdfPCell(table2);//将table放到cell中
table.addCell(cell);//将cell放到外层的table中去
return table;
}
效果和刚才是完全⼀样的,这种⽅法的关键就是将新建的table放到cell中,然后把cell放到外层table中去。这种⽅法明显⽐刚才的⿇烦很多,我之所以拿出来是因为我在⽤⽼版本的jar包中的合并⾏的⽅法突然不起作⽤了,然后临时想了这个办法出来,⾄于为什么⽅法失效报错我到现在还没弄明⽩。。。
在表格中加⼊单选框/复选框
之所以提这个,在我⾃⼰项⽬中就有这样的需求,⽽且在⽹上这种资料⾮常少,我这也是在官⽹上看到的,遂记录⼀下,先看⼀下单选框怎么加,代码如下
public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
PdfPTable table = new PdfPTable(2);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 20;
cell = new PdfPCell(new Phrase("one"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("two"));
cell.setFixedHeight(size);
cell.setColspan(2);
table.addCell(cell);
cell = new PdfPCell(new Phrase("three"));
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new Phrase("four"));
cell.setFixedHeight(size);
cell.setColspan(2);
table.addCell(cell);
cell = new PdfPCell(new Phrase("five"));
cell.setColspan(1);//设置所占列数
//cell.setRowspan(2);//合并⾏
cell.setFixedHeight(size*2);//设置⾼度
cell.setHorizontalAlignment(Element.ALIGN_CENTER);//设置⽔平居中
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);//设置垂直居中
table.addCell(cell);
PdfPTable table2 = new PdfPTable(1);//新建⼀个table
cell = new PdfPCell(new Phrase("six"));
cell.setFixedHeight(size);
table2.addCell(cell);
cell = new PdfPCell(new Phrase("seven"));
cell.setFixedHeight(size);
table2.addCell(cell);
cell = new PdfPCell(table2);//将table放到cell中
table.addCell(cell);//将cell放到外⾯的table中去
Phrase phrase1 = new Phrase();
Chunk chunk1 = new Chunk("        YES");
Chunk chunk2 = new Chunk("          NO");
phrase1.add(chunk1);
phrase1.add(chunk2);
cell = new PdfPCell(phrase1);
cell.setColspan(2);
table.addCell(cell);
//增加两个单选框
PdfFormField  ateRadioButton(writer, true);
radiogroup.setFieldName("salesModel");
Rectangle rect1 = new Rectangle(110, 722, 120, 712);
Rectangle rect2 = new Rectangle(165, 722, 175, 712);
RadioCheckField radio1 = new RadioCheckField(writer, rect1, null, "self-support" ) ;
RadioCheckField radio2 = new RadioCheckField(writer, rect2, null, "cooprate") ;
radio2.setChecked(true);
PdfFormField radiofield1 = RadioField();
PdfFormField radiofield2 = RadioField();
radiogroup.addKid(radiofield1);
radiogroup.addKid(radiofield2);
writer.addAnnotation(radiogroup);
return table;
}
  效果如图
这个最烦的地⽅就是⾃⼰要去⼀点点试单选框的参数来调整位置,就是 Rectangle rect1 = new Rectangle()⾥⾯的参数,然后想让哪个radio被选中就⽤setChecked(true)⽅法就OK了
解决中⽂字体的问题
在之前的例⼦中我⽤的都是英⽂字,但是中⽂字体是⼀个不得不解决的问题,根据我去⽹上搜集到的资料⼤抵有以下⼏个⽅法:
1. 使⽤windows系统⾃带的字体,这种事最省事,也是最简单的,代码如下
public static PdfPTable createTable(PdfWriter writer) throws DocumentException, IOException{
PdfPTable table = new PdfPTable(2);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 20;
Font font = new ateFont("C:/Windows/Fonts/SIMYOU.TTF",BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED));
cell = new PdfPCell(new Phrase("显⽰中⽂",font));
cell.setFixedHeight(size);
cell.setColspan(2);
table.addCell(cell);
return table;
}
2. 使⽤itext-asian.jar中的中⽂字体,代码如下:
Font font = new ateFont( "STSongStd-Light" ,"UniGB-UCS2-H",BaseFont.NOT_EMBEDDED));
在这⾥可能会报Font 'STSongStd-Light' with 'UniGB-UCS2-H' is not recognized.的错,⽹上都说是语⾔包中的包名有问题,但是我把itexitextpdf包的版本从5.0.6改成5.4.3就解决了,我把语⾔包解压后发现路径是对的,应该是后⾯的版本已经把语⾔包中的包名解决了,那为什么5.0.6的版本不⾏呢,反正我到现在还没到原因,如果有谁知道的话欢迎留⾔告知。
  3.使⽤⾃⼰下载的资源字体。这种⽅法可以说是最⿇烦的,但是也是最有效的。⿇烦是因为⾃⼰要去下载字体⽂件,但是效果确是最好的,能够应对各种系统,各种状况,下⾯会提到,先看⼀下我的⽂件路径
然后是代码
Font font = new ateFont( "/f",BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED));
其实跟第⼀个是差不多的,只是⼀个是读系统的⽂件,⼀个是读项⽬⾃⼰的⽂件。 
在springboot中导出pdf⽂件
前⾯做了这么多铺垫,终于来到我们最关键的地⽅了,前⾯的东西都是放到java项⽬中去跑的,没有太多实际⽤处,在web项⽬中⽤到才算真正的应⽤,假设我们有⼀个产品类,我们的需求就是把产品数据导成pdf⽂件,代码如下:
产品Product类
public class Product {
private String productName;
private String productCode;
private float price;
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public Product(String productName, String productCode, float price) {
super();
this.productName = productName;
this.productCode = productCode;
this.price = price;
}
}
接下来是controller
//打印pdf
@RequestMapping("printPdf")
public ModelAndView printPdf() throws Exception{
Product product1 = new Product("产品⼀","cp01",120);springboot中文
Product product2 = new Product("产品⼀","cp01",120);
Product product3 = new Product("产品⼀","cp01",120);
List<Product>products = new ArrayList<>();
products.add(product1);
products.add(product2);
products.add(product3);
Map<String, Object> model = new HashMap<>();
model.put("sheet", products);
return new ModelAndView(new ViewPDF(), model);
}
然后是ViewPDF类
public class ViewPDF extends AbstractPdfView {
@Override
protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String fileName = new Date().getTime()+"_quotation.pdf"; // 设置response⽅式,使执⾏此controller时候⾃动出现下载页⾯,⽽⾮直接使⽤excel打开
response.setCharacterEncoding("UTF-8");
response.setContentType("application/pdf");
response.setHeader("Content-Disposition","filename=" + new Bytes(), "iso8859-1"));
List<Product> products = (List<Product>) ("sheet");
PdfUtil pdfUtil = new PdfUtil();
}
}
在这⾥有两个值得注意的地⽅:
  1. 仔细观察这个类,看⽗类的bulidPdfDocument(),发现其中的document要求的是这个Document版本的,这就⾮常坑了,意味着之前的itextpdf-5.0.6.jar不能再⽤了,只能
⽼版本的(改名之前的)itext-2.1.7.jar了。
  2. 如果不想要这种直接在浏览器中预览的效果,⽽是直接下载⽂件的话就把response.setHeader()⽅法⾥⾯的参数改成下⾯的,相当于加了⼀个attachment,以附件形式下载
response.setHeader("Content-Disposition","attachment;filename=" + new Bytes(), "iso8859-1"));
最后是PdfUtil类
public class PdfUtil {
public void createPDF(Document document,PdfWriter writer, List<Product> products) throws IOException {
//Document document = new Document(PageSize.A4);
try {
document.addTitle("sheet of product");
document.addAuthor("scurry");
document.addSubject("product sheet.");
document.addKeywords("product.");
document.open();
PdfPTable table = createTable(writer,products);
document.add(table);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (DocumentException e) {
e.printStackTrace();
} finally {
document.close();
}
}
public static PdfPTable createTable(PdfWriter writer,List<Product> products) throws IOException, DocumentException {
PdfPTable table = new PdfPTable(3);//⽣成⼀个两列的表格
PdfPCell cell;
int size = 20;
Font font = new ateFont("C://Windows//Fonts//f", BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED));
for(int i = 0;i<products.size();i++) {
cell = new PdfPCell(new (i).getProductCode(),font));//产品编号
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new (i).getProductName(),font));//产品名称
cell.setFixedHeight(size);
table.addCell(cell);
cell = new PdfPCell(new (i).getPrice()+"",font));//产品价格
cell.setFixedHeight(size);
table.addCell(cell);
}
return table;
}
}
这同时也意味着如果项⽬要打包部署到linux服务器上去的话,前两种的中⽂解决办法都不好使了,只能下载中⽂字体资源⽂件,然⽽这其中⼜有⼀个坑,打包后⽂件路径读取不到。
所以要以加载资源⽂件的形式读取⽂件,代码如下
BaseFont baseFont = ateFont(new ClassPathResource("/f").getPath(), BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);
效果如下图:
在这⾥既可以下载也可以打印。
  到这⾥我这踩了⼀路的坑都已经说完了,如果之后遇到新的问题我也持续更新这篇博客的。总的来说导出pdf真的是⼀件很繁琐的事情,还是那句话如果能导excel的话就尽量导excel吧。。。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。