史上最全的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";
    pdf.createPDF(filename);
    System.out.println("打印完成");
    
}
  public void createPDF(String filename) throws IOException {
    Document document = new Document(PageSize.A4);
    try {
        PdfWriter.getInstance(document, new FileOutputStream(filename));
        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大多數是以表格的形式,所以我就直接切入主題了,這里要用到一個很關鍵的類com.itextpdf.text.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 = PdfWriter.getInstance(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  radiogroup=PdfFormField.createRadioButton(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 = radio1.getRadioField();
    PdfFormField radiofield2 = radio2.getRadioField();
    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 Font(BaseFont.createFont("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 Font(BaseFont.createFont( "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 Font(BaseFont.createFont( "/simsun.ttf",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);
      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 String(fileName.getBytes(), "iso8859-1"));
      List<Product> products = (List<Product>) model.get("sheet");
      PdfUtil pdfUtil = new PdfUtil();
      pdfUtil.createPDF(document, writer, products);
  }
}

在這里有兩個值得注意的地方:

  1. 仔細觀察這個類,看父類的bulidPdfDocument(),發現其中的document要求的是這個com.lowagie.text.Document版本的,這就非常坑了,意味着之前的itextpdf-5.0.6.jar不能再用了,只能用老版本的(改名之前的)itext-2.1.7.jar了。

  2. 如果不想要這種直接在瀏覽器中預覽的效果,而是直接下載文件的話就把response.setHeader()方法里面的參數改成下面的,相當於加了一個attachment,以附件形式下載

response.setHeader("Content-Disposition","attachment;filename=" + new String(fileName.getBytes(), "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 Font(BaseFont.createFont("C://Windows//Fonts//simfang.ttf", BaseFont.IDENTITY_H,
      BaseFont.NOT_EMBEDDED));   
    for(int i = 0;i<products.size();i++) {
      cell = new PdfPCell(new Phrase(products.get(i).getProductCode(),font));//產品編號
      cell.setFixedHeight(size);
      table.addCell(cell);
      cell = new PdfPCell(new Phrase(products.get(i).getProductName(),font));//產品名稱
      cell.setFixedHeight(size);
      table.addCell(cell);
      cell = new PdfPCell(new Phrase(products.get(i).getPrice()+"",font));//產品價格
      cell.setFixedHeight(size);
      table.addCell(cell);
    }
    return table;
  }
}

這同時也意味着如果項目要打包部署到linux服務器上去的話,前兩種的中文解決辦法都不好使了,只能下載中文字體資源文件,然而這其中又有一個坑,打包后文件路徑讀取不到。

所以要以加載資源文件的形式讀取文件,代碼如下

BaseFont baseFont = BaseFont.createFont(new ClassPathResource("/simsun.ttf").getPath(), BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);

效果如下圖:

在這里既可以下載也可以打印。

  到這里我這踩了一路的坑都已經說完了,如果之后遇到新的問題我也持續更新這篇博客的。總的來說導出pdf真的是一件很繁瑣的事情,還是那句話如果能導excel的話就盡量導excel吧。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM