➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
用 Next.js 建立 React 網站Spring Boot 外置 classpath

JasperReports

Table of contents

1 JasperReports 簡介

JasperReports 係一個 reporting 既工具,以純文字 template 既形式保存 report 既設計。佢亦提供相應既 Java library 畀程式去生成 report 檔案。
JasperReports 支援多種 report 檔案格式,例如 PDF、Microsoft Word、HTML,亦支援 PDF 內 embed fonts、PDF 既原生檔案加密功能。

2 開發流程

  1. 用一個商業 email 註冊一個 Jaspersoft community 帳號。
  2. 下載 Jasper Studio community 版。
  3. 打開 Jasper Studio,用 Jaspersoft community 帳號登入。
  4. 用 Jasper Studio 製作 report template。
  5. 將檔案保存為 JRXML 格式(亦可以用 Jasper Studio build 成 .jasper 檔案)。
  6. 寫一個 Java 程式去生成 report 檔案。
  7. 根據需要而執行呢個 Java 程式。

3 .jrxml vs .jasper 檔案格式

檔案格式描述
.jrxml係裝住 template layout 設計既 XML 格式。
.jasper.jrxml 經過 compilation 而得出既 Java bytecode 格式。

4 JasperReports 6 vs 7

JasperReports 7 比起 JasperReports 6 既最大分別:
  • JasperReports 7 將 JasperReports 6 既部分 API classes 分拆左出黎做新既 Maven modules。
  • JasperReports 6、7 既 JRXML 檔案、Java libraries 唔相容。
  • 我地可以用 Jasper Studio 7 將 JasperReports 6 既 JRXML 轉換成 JasperReports 7 既格式。

5 Report template 例子(JRXML)

JRXML:
  • 係 XML 格式。
  • 裝住 template layout 設計。
  • 有 sub-report 既概念,可以 reference 另一個 template 既 .jasper 檔案。
  • 可以用 Jasper Studio 或者 JasperReports Java library build 成 .jasper 檔案。
  • 應該被視為 source code,並且保存喺 version control 工具。

5.1 JasperReports 6 既 JRXML

1<?xml version="1.0" encoding="UTF-8"?> 2<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports 5 http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" 6 name="SampleReport" 7 pageWidth="595" 8 pageHeight="842" 9 columnWidth="555" 10 leftMargin="20" 11 rightMargin="20" 12 topMargin="20" 13 bottomMargin="20"> 14 15 <scriptlet name="myScriptlet" class="code.MySampleScriptlet" /> 16 17 <parameter name="recordCount" class="java.lang.Integer"> 18 <defaultValueExpression><![CDATA[200]]></defaultValueExpression> 19 </parameter> 20 21 <queryString> 22 <![CDATA[SELECT TOP ($P{recordCount}) id, full_name, description FROM tbl_user]]> 23 </queryString> 24 25 <field name="id" class="java.lang.Integer" /> 26 <field name="full_name" class="java.lang.String" /> 27 <field name="description" class="java.lang.String" /> 28 29 <title> 30 <band height="50"> 31 <textField> 32 <reportElement x="0" y="0" width="555" height="30" /> 33 <textElement> 34 <font fontName="SmoochSans" size="8" isBold="true" isItalic="false" /> 35 </textElement> 36 <textFieldExpression><![CDATA[$P{myScriptlet_SCRIPTLET}.mySampleMethod()]]></textFieldExpression> 37 </textField> 38 </band> 39 </title> 40 41 <columnHeader> 42 <band height="30"> 43 <staticText> 44 <reportElement x="0" y="0" width="50" height="30" /> 45 <textElement> 46 <font fontName="SmoochSans" size="8" isBold="true" isItalic="false" /> 47 </textElement> 48 <text><![CDATA[ID]]></text> 49 </staticText> 50 <staticText> 51 <reportElement x="50" y="0" width="100" height="30"/> 52 <textElement> 53 <font fontName="SmoochSans" size="8" isBold="true" isItalic="false" /> 54 </textElement> 55 <text><![CDATA[Name]]></text> 56 </staticText> 57 <staticText> 58 <reportElement x="150" y="0" width="400" height="30" /> 59 <textElement> 60 <font fontName="SmoochSans" size="8" isBold="true" isItalic="false" /> 61 </textElement> 62 <text><![CDATA[Description]]></text> 63 </staticText> 64 </band> 65 </columnHeader> 66 67 <detail> 68 <band height="15"> 69 <textField> 70 <reportElement x="0" y="0" width="50" height="15" /> 71 <textElement> 72 <font fontName="SmoochSans" size="8" isBold="false" isItalic="false" /> 73 </textElement> 74 <textFieldExpression><![CDATA[$F{id}]]></textFieldExpression> 75 </textField> 76 <textField> 77 <reportElement x="50" y="0" width="100" height="15" /> 78 <textElement> 79 <font fontName="SmoochSans" size="8" isBold="false" isItalic="false" /> 80 </textElement> 81 <textFieldExpression><![CDATA[$F{full_name}]]></textFieldExpression> 82 </textField> 83 <textField> 84 <reportElement x="150" y="0" width="400" height="15" /> 85 <textElement> 86 <font fontName="SmoochSans" size="8" isBold="false" isItalic="false" /> 87 </textElement> 88 <textFieldExpression><![CDATA[$F{description}]]></textFieldExpression> 89 </textField> 90 </band> 91 </detail> 92</jasperReport>

5.2 JasperReports 7 既 JRXML

我地可以用 Jasper Studio 7 將 JasperReports 6 既 JRXML 轉換成 JasperReports 7 既格式。
步驟:用 Jasper Studio 打開 JasperReports 6 既 JRXML 檔案,再保存,佢會自動保存成 JasperReports 7 既格式。
可以睇到 JasperReports 7 既 JRXML 檔案內容同 JasperReports 6 既 JRXML 檔案內容都有明顯既分別。
1<?xml version="1.0" encoding="UTF-8"?> 2<jasperReport 3 name="SampleReport" 4 language="java" 5 pageWidth="595" 6 pageHeight="842" 7 columnWidth="555" 8 leftMargin="20" 9 rightMargin="20" 10 topMargin="20" 11 bottomMargin="20" 12 scriptletClass="code.MySampleScriptlet"> 13 14 <parameter name="recordCount" class="java.lang.Integer"> 15 <defaultValueExpression><![CDATA[200]]></defaultValueExpression> 16 </parameter> 17 18 <query language="sql"> 19 <![CDATA[SELECT TOP ($P{recordCount}) id, full_name, description FROM tbl_user]]> 20 </query> 21 22 <field name="id" class="java.lang.Integer" /> 23 <field name="full_name" class="java.lang.String" /> 24 <field name="description" class="java.lang.String" /> 25 26 <title height="50"> 27 <element kind="textField" x="0" y="0" width="555" height="30" fontName="SmoochSans" fontSize="8.0" italic="false" bold="true"> 28 <expression><![CDATA[$P{REPORT_SCRIPTLET}.mySampleMethod()]]></expression> 29 </element> 30 </title> 31 32 <columnHeader height="30"> 33 <element kind="staticText" x="0" y="0" width="50" height="30" fontName="SmoochSans" fontSize="8.0" italic="false" bold="true"> 34 <text><![CDATA[ID]]></text> 35 </element> 36 <element kind="staticText" x="50" y="0" width="100" height="30" fontName="SmoochSans" fontSize="8.0" italic="false" bold="true"> 37 <text><![CDATA[Name]]></text> 38 </element> 39 <element kind="staticText" x="150" y="0" width="400" height="30" fontName="SmoochSans" fontSize="8.0" italic="false" bold="true"> 40 <text><![CDATA[Description]]></text> 41 </element> 42 </columnHeader> 43 44 <detail> 45 <band height="15"> 46 <element kind="textField" x="0" y="0" width="50" height="15" fontName="SmoochSans" fontSize="8.0" italic="false" bold="false"> 47 <expression><![CDATA[$F{id}]]></expression> 48 </element> 49 <element kind="textField" x="50" y="0" width="100" height="15" fontName="SmoochSans" fontSize="8.0" italic="false" bold="false"> 50 <expression><![CDATA[$F{full_name}]]></expression> 51 </element> 52 <element kind="textField" x="150" y="0" width="400" height="15" fontName="SmoochSans" fontSize="8.0" italic="false" bold="false"> 53 <expression><![CDATA[$F{description}]]></expression> 54 </element> 55 </band> 56 </detail> 57</jasperReport>

6 動手寫 report 生成程式

6.1 程式流程

  1. Compile JRXML 做 .jasper 檔案,或者讀取 pre-compiled 既 .jasper 檔案,會得到一個 JasperReport object。
  2. 創建一個 JDBC database connection 或者 data source。
  3. 用 data 去 fill 個 JasperReport object,我地可以提供 JDBC database connection 或者 data source,會得到一個 JasperPrint object。
  4. Export 個 JasperPrint object 做不同格式既 report 檔案,例如 PDF。

6.2 Project structure

  • src/main
    • /java
      • /code
        • Main.java
        • MySampleScriptlet.java
    • /resources
      • /fonts
        • /SmoochSans
          • SmoochSans-Bold.ttf
          • SmoochSans-Regular.ttf
        • font-config.xml
      • /templates
        • sample.jasper
        • sample.jrxml
      • jasperreports_extension.properties
  • pom.xml
註:
  • Report templates 唔一定要放喺 src/main/resources 裡面,只要 Java code 用岩 file path 就會讀到。
  • 我地會用一隻叫 Smooch Sans 既 font 黎做 custom font 既例子。

6.3 寫 JasperReports 6 既程式

6.3.1 Maven dependencies

<dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>6.21.4</version> </dependency>

6.3.2 寫 Java code

1import net.sf.jasperreports.engine.export.JRPdfExporter; 2 3public void testGenerateReport() throws Exception { 4 5 final Collection<Map<String, ?>> data = new JsonMapper().readValue(""" 6 [ 7 { "id": 1001, "full_name": "Michael", "description": "This is Michael" }, 8 { "id": 1002, "full_name": "Jack", "description": "This is not Jack" } 9 ] 10 """, Collection.class); 11 final JRDataSource dataSource = new JRMapCollectionDataSource(data); 12 13 final OutputStream os = new FileOutputStream("reports/sample.pdf"); 14 final JasperReport reportTemplate = getTemplate(); 15 final JasperPrint filledReport = fillTemplate(reportTemplate, dataSource); 16 17 try (os) { 18 exportReportAsPdf(filledReport, os); 19 } 20} 21 22private JasperReport getTemplate() throws Exception { 23 24 // XXX 揀其中一個方法 25 // 1. Runtime compilation 第一次會好慢 26 // 2. Load pre-compiled 既 .jasper 會快啲 27 28 return compileTemplate(); 29 return loadTemplate(); 30} 31 32private JasperReport compileTemplate() throws Exception { 33 34 final InputStream is = new FileInputStream("path/to/templates/sample.jrxml"); 35 36 try (is) { 37 return JasperCompileManager.compileReport(is); 38 } 39} 40 41private JasperReport loadTemplate() throws Exception { 42 return (JasperReport) JRLoader.loadObjectFromFile("path/to/templates/sample.jasper"); 43} 44 45private JasperPrint fillTemplate(JasperReport reportTemplate, JRDataSource dataSource) throws Exception { 46 return JasperFillManager.fillReport(reportTemplate, null, dataSource); 47} 48 49private void exportReportAsPdf(JasperPrint filledReport, OutputStream os) throws Exception { 50 51 final JRPdfExporter exporter = new JRPdfExporter(DefaultJasperReportsContext.getInstance()); 52 exporter.setExporterInput(new SimpleExporterInput(filledReport)); 53 exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(os)); 54 55 exporter.exportReport(); 56}

6.4 寫 JasperReports 7 既程式

6.4.1 Maven dependencies

1<dependency> 2 <groupId>net.sf.jasperreports</groupId> 3 <artifactId>jasperreports</artifactId> 4 <version>7.0.1</version> 5</dependency> 6 7<dependency> 8 <groupId>net.sf.jasperreports</groupId> 9 <artifactId>jasperreports-jdt</artifactId> 10 <version>7.0.1</version> 11</dependency> 12 13<dependency> 14 <groupId>net.sf.jasperreports</groupId> 15 <artifactId>jasperreports-pdf</artifactId> 16 <version>7.0.1</version> 17</dependency>

6.4.2 寫 Java code

JasperReports 7 既 Java code 同 JasperReports 6 冇乜分別,只係因為 JasperReports 7 將 JasperReports 6 既部分 API classes 分拆左出黎做新既 Maven modules,所以我地需要改部分 import statements 既 package references。
1import net.sf.jasperreports.pdf.JRPdfExporter; // 黎自 Jasper 7 既 jasperreports-pdf Maven module 2 3public void testGenerateReport() throws Exception { 4 5 final Collection<Map<String, ?>> data = new JsonMapper().readValue(""" 6 [ 7 { "id": 1001, "full_name": "Michael", "description": "This is Michael" }, 8 { "id": 1002, "full_name": "Jack", "description": "This is not Jack" } 9 ] 10 """, Collection.class); 11 final JRDataSource dataSource = new JRMapCollectionDataSource(data); 12 13 final OutputStream os = new FileOutputStream("reports/sample.pdf"); 14 final JasperReport reportTemplate = getTemplate(); 15 final JasperPrint filledReport = fillTemplate(reportTemplate, dataSource); 16 17 try (os) { 18 exportReportAsPdf(filledReport, os); 19 } 20} 21 22private JasperReport getTemplate() throws Exception { 23 24 // XXX 揀其中一個方法 25 // 1. Runtime compilation 第一次會好慢 26 // 2. Load pre-compiled 既 .jasper 會快啲 27 28 return compileTemplate(); 29 return loadTemplate(); 30} 31 32private JasperReport compileTemplate() throws Exception { 33 34 final InputStream is = new FileInputStream("path/to/templates/sample.jrxml"); 35 36 try (is) { 37 return JasperCompileManager.compileReport(is); 38 } 39} 40 41private JasperReport loadTemplate() throws Exception { 42 return (JasperReport) JRLoader.loadObjectFromFile("path/to/templates/sample.jasper"); 43} 44 45private JasperPrint fillTemplate(JasperReport reportTemplate, JRDataSource dataSource) throws Exception { 46 return JasperFillManager.fillReport(reportTemplate, null, dataSource); 47} 48 49private void exportReportAsPdf(JasperPrint filledReport, OutputStream os) throws Exception { 50 51 final JRPdfExporter exporter = new JRPdfExporter(DefaultJasperReportsContext.getInstance()); 52 exporter.setExporterInput(new SimpleExporterInput(filledReport)); 53 exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(os)); 54 55 exporter.exportReport(); 56}

6.5 Report data 獲取方式

6.5.1 JDBC database connection

JasperReports 既 report template(JRXML)可以自帶 SQL queries。
我地只需要提供一個 JDBC database connection 黎 fill 個 JasperReport object 就得,JasperReports 既 Java API 背後會自己執行 SQL queries。

6.5.2 Data source

另一個方式係我地先自行拎左 data,再用對應既 JRDataSource implementation class wrap 住佢,再用佢黎 fill 個 JasperReport object。
Data structure 一般都會係 N 個 rows,每個 row 有一樣既 fields,我地可以用:
Java data structureJasperReports wrapper class描述
List<Map<String, ?>>JRMapCollectionDataSource一個 generic key-value pair 既 data structure。
List<XxxDto>JRBeanCollectionDataSource一個特定 Java type(VO/DTO/entity class)既 data structure。

6.6 Custom font

jasperreports_extension.properties
net.sf.jasperreports.extension.registry.factory.fonts=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory net.sf.jasperreports.extension.simple.font.families.mySampleFontFamily=fonts/font-config.xml
註:mySampleFontFamily 可以係任何 property key 名。
fonts/font-config.xml
1<?xml version="1.0" encoding="UTF-8"?> 2<fontFamilies> 3 4 <fontFamily name="SmoochSans"> 5 <normal> 6 <ttf><![CDATA[fonts/SmoochSans/SmoochSans-Regular.ttf]]></ttf> 7 <pdf><![CDATA[fonts/SmoochSans/SmoochSans-Regular.ttf]]></pdf> 8 </normal> 9 <bold><![CDATA[fonts/SmoochSans/SmoochSans-Bold.ttf]]></bold> 10 <pdfEncoding><![CDATA[Cp1252]]></pdfEncoding> 11 <pdfEmbedded><![CDATA[true]]></pdfEmbedded> 12 <exportFonts/> 13 </fontFamily> 14 15</fontFamilies>
註:
  • 喺呢個例子裡面我地用左 Smooch Sans 既 custom font,而 JRXML 檔案裡面都有 reference 到 fontFamilyname,即係 SmoochSans
  • pdfEmbedded 設定做 true 就可以將 custom font embed 入個 report PDF 檔案裡面。

6.7 JasperReports scriptlet

JasperReports 仲有一個功能叫 scriptlet,可以畀我地寫任意既 Java code 黎 fill data 落個 report template 度。
  • Scriptlets 既 Java classes 需要 extend JRAbstractScriptlet 或者 JRDefaultScriptlet
  • 我地可以寫一啲 user-defined methods,然後用 $P{REPORT_SCRIPTLET}.mySampleMethod() 去 invoke 個 method。
    • 喺 JasperReports 6、7,我地可以用 REPORT_SCRIPTLET 既 parameter 黎 reference <jasperReport>scriptletClass
    • 喺 JasperReports 6(7 試過唔 work),我地仲可以用 <scriptlet name>_SCRIPTLET 既 parameter 黎 reference <scriptlet>class
MySampleScriptlet.java
1public class MySampleScriptlet extends JRDefaultScriptlet { 2 3 public String mySampleMethod() { 4 return "Sample Report"; 5 } 6}

6.8 PDF 加密

加入呢個 method:
1private PdfExporterConfiguration getPdfConfig(String ownerPassword, String userPassword) { 2 3 final SimplePdfExporterConfiguration config = new SimplePdfExporterConfiguration(); 4 config.setEncrypted(true); 5 config.set128BitKey(true); 6 config.setOwnerPassword(ownerPassword); 7 config.setUserPassword(userPassword); 8 config.setPermissions( // XXX 可以自己 fine-tune permissions 9 PdfWriter.ALLOW_ASSEMBLY 10 | PdfWriter.ALLOW_COPY 11 | PdfWriter.ALLOW_DEGRADED_PRINTING 12 | PdfWriter.ALLOW_FILL_IN 13 | PdfWriter.ALLOW_MODIFY_ANNOTATIONS 14 | PdfWriter.ALLOW_MODIFY_CONTENTS 15 | PdfWriter.ALLOW_PRINTING 16 | PdfWriter.ALLOW_SCREENREADERS); 17 18 return config; 19}
修改 method exportReportAsPdf
1private void exportReportAsPdf(JasperPrint filledReport, OutputStream os) throws Exception { 2 3 final JRPdfExporter exporter = new JRPdfExporter(DefaultJasperReportsContext.getInstance()); 4 exporter.setExporterInput(new SimpleExporterInput(filledReport)); 5 exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(os)); 6 exporter.setConfiguration(getPdfConfig("<owner password>", "<user password>")); // 加呢句 7 8 exporter.exportReport(); 9}
註:呢啲 code 係 JasperReports 6、7 都通用,只係 import statements 既 Java packages 會有分別。

7 Report 生成結果

執行程式之後,就會生成到一個咁既 PDF 檔案:

8 參考資料