Warm-up Quick Fire: Retention Check (from 4.1 Excel Sanitization)
สถานการณ์จำลอง: ในการตั้งค่า Pixel-Perfect Export ของ JasperReports 7.0.x หากคุณมี Detail Band ที่ขยายตัวได้ตามเนื้อหา (stretchType="containerHeight") แต่พบว่าแถวใน Microsoft Excel ถูกล็อคความสูงแบบตายตัวจนตัวอักษรบรรทัดล่างถูกตัดทิ้ง (Row Height Clipping) คุณต้องกำหนด Export Property ตัวใดให้กับตัวรายงาน?
4.2 Contextual Anchoring: The Multi-Sheet Architecture Dilemma
Why simple pagination breaks down in Enterprise Big-Data Spreadsheets
การสร้างรายงานทางการเงินหรือธุรกรรม Transaction รายปี (General Ledger) มักมีข้อมูลเกินหลายแสนแถว หากพยายามยัดลงใน Single Sheet ระบบจะชนข้อจำกัดขีดสุดของ Excel (.xlsx รองรับ $1,048,576$ แถว) และหาก Export Engine ไม่ได้ทำงานในโหมด Streaming (Apache POI SXSSF) ตัว Java DOM Tree จะสะสมวัตถุเซลล์หลายสิบล้านโหนดบน Old Gen Heap จนเกิด java.lang.OutOfMemoryError: Java heap space ทันที
ผู้บริหารต้องการแยกแท็บตาม Department หรือ Business Unit แต่เมื่อข้อมูลมีชื่อสาขาซ้ำกัน หรือมีอักขระต้องห้ามของ Microsoft Excel เช่น / \ ? * : [ ] หรือชื่อยาวเกิน 31 อักขระ POI จะพ่นข้อยกเว้น หรือสร้างไฟล์ที่ทำให้ Excel แจ้งเตือน "We found a problem with some content in 'report.xlsx'..." จนผู้ใช้งานสูญเสียความเชื่อมั่น
ในเวอร์ชัน 7.0.x สถาปัตยกรรมรองรับการตัดแท็บผ่าน Group Pagination (isStartNewPage="true") ผสานคุณสมบัติ net.sf.jasperreports.export.xls.sheet.name ระดับ Element Property Expression ที่คำนวณแบบ Dynamic ตอน Filler วนลูป โดยทำงานร่วมกับ Apache POI SXSSF Disk Swapping ได้ 100%
🏛️ Dynamic Core Pillars (4 เสาหลักการแยกแท็บข้อมูลระดับ Enterprise)
Group-Triggered Page Splitting
การสั่งให้ Engine สร้างแท็บใหม่ต้องพึ่งพา Logical Page Break ของ JRVerticalFiller ผ่านแอตทริบิวต์ isStartNewPage="true" ในระดับ <group> หรือใส่ <element kind="break" type="Page"/>
Dynamic Sheet Name Resolution
การผูกชื่อแท็บแบบ Real-time ด้วย Property Expression: net.sf.jasperreports.export.xls.sheet.name วางไว้ใน Group Header Element เพื่อให้ Exporter อ่านค่าและ Apply ลง Apache POI Sheet ณ จังหวะ Render Page
Hard Row-Count Auto-Splitting
การป้องกันไฟล์แตกเมื่อข้อมูลทะลุขีดจำกัด ด้วย net.sf.jasperreports.export.xls.max.rows.per.sheet Engine จะคำนวณแถวสะสม และทำการ Force New Sheet พร้อมรันหมายเลข Index ต่อท้ายอัตโนมัติ
SXSSF Streaming & Temp Offload
การเปิดใช้งาน JRXlsxExporter ร่วมกับ Row Access Window (เช่น 100 แถวบน RAM) เพื่อ Flush แถวก่อนหน้าลง .gz ในดิสก์ชั่วคราว ป้องกัน Garbage Collection Thrashing
Engine Internals: How JRVerticalFiller Interacts with JRXlsxExporter
เมื่อรายงานถูก Execute ผ่าน JasperFillManager.fillReport(...) ตัว JRVerticalFiller จะประมวลผลข้อมูลทีละ Record หากมี Group Break ที่กำหนด isStartNewPage="true" ตัว Filler จะทำการปิด JRPrintPage เดิม และเริ่มหน้าพิมพ์ใหม่ โดยเก็บ Print Elements ทั้งหมดไว้ในหน่วยความจำ (หรือ Virtualizer Paged File)
ณ จังหวะ Export ด้วย JRXlsxExporter พารามิเตอร์ net.sf.jasperreports.export.xls.one.page.per.sheet=true จะสั่งการลูป for (JRPrintPage page : jasperPrint.getPages()) ให้ทำการสร้าง POI Sheet ใหม่ขึ้นมา 1 แท็บต่อ 1 Logical Page!
| Production Hazard | Root Cause Mechanism | Symptom / Exception | Architectural Defense |
|---|---|---|---|
| Duplicate Sheet Name Crash | Field ที่นำมาตั้งชื่อ Sheet มีค่าซ้ำกันใน Record ต่าง Group (เช่น ลูกค้ามี Order 2 ช่วงเวลา) | IllegalArgumentException: The workbook already contains a sheet named '...' | ผูก Expression โดยใส่ Unique ID หรือสร้าง Variable นับ Sequence: $F{DeptName} + "_" + $V{REPORT_COUNT} |
| Excel Illegal Characters Violation | ชื่อ Sheet มีอักขระต้องห้ามตามสเปก OpenXML: : \ / ? * [ ] |
IllegalArgumentException: Invalid char (/) found at index (...) in sheet name | Sanitize ข้อมูลใน Expression ด้วย Regex: $F{DeptName}.replaceAll("[\\\\/\\?\\*\\[\\]:]", "_") |
| Sheet Name Length Exceeded | ข้อจำกัดของ Excel BIFF8/OpenXML ไม่อนุญาตให้ชื่อแท็บเกิน 31 ตัวอักษร | IllegalArgumentException: sheetName length must be between 1 and 31 | Truncate ด้วย String substring: $F{Name}.length() > 30 ? $F{Name}.substring(0, 30) : $F{Name} |
| Memory Exhaustion (OOM) on Large Multi-sheet | ใช้ HSSFWorkbook (XLS โบราณ) หรือใช้ SXSSFWorkbook โดยไม่ได้ตั้งค่า Temp File Swap | java.lang.OutOfMemoryError: Java heap space during ZipOutputStream flush | ตั้งค่า JVM Properties net.sf.jasperreports.export.xls.use.temporary.file=true และกำหนด Storage Path ใน Container |
Strict 7.0.x Architecture Implementation
โค้ดสถาปัตยกรรมด้านล่างคือมาตรฐาน Enterprise สำหรับ JasperReports 7.0.3+ ที่ใช้แบ่งแท็บข้อมูลตามฝ่าย (DepartmentGroup) โดยแต่ละแท็บจะมีชื่อตามรหัสแผนก พร้อมฟอกชื่อให้อยู่ในขอบเขต 31 ตัวอักษรและปราศจากอักขระต้องห้าม:
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
name="MultiSheetEnterpriseReport"
pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20"
uuid="c3f81e2e-8e4a-4a61-9c8e-32b0a1a01234">
<!-- Global Export Properties for POI XLSX Multi-Sheet Splitting -->
<property name="net.sf.jasperreports.export.xls.one.page.per.sheet" value="true"/>
<property name="net.sf.jasperreports.export.xls.remove.empty.space.between.rows" value="true"/>
<property name="net.sf.jasperreports.export.xls.remove.empty.space.between.columns" value="true"/>
<property name="net.sf.jasperreports.export.xls.detect.cell.type" value="true"/>
<property name="net.sf.jasperreports.export.xls.white.page.background" value="false"/>
<property name="net.sf.jasperreports.export.xls.use.temporary.file" value="true"/>
<field name="departmentId" class="java.lang.String"/>
<field name="departmentName" class="java.lang.String"/>
<field name="employeeCode" class="java.lang.String"/>
<field name="employeeName" class="java.lang.String"/>
<field name="salary" class="java.math.BigDecimal"/>
<!-- Group definition driving the sheet split -->
<group name="DepartmentGroup" isStartNewPage="true" isResetPageNumber="true">
<expression><![CDATA[$F{departmentId}]]></expression>
<groupHeader>
<band height="55">
<!-- Sheet Name Controller: Attached to group header text field -->
<element kind="textField" x="0" y="0" width="555" height="30">
<propertyExpression name="net.sf.jasperreports.export.xls.sheet.name">
<![CDATA[
($F{departmentId} + "_" + $F{departmentName})
.replaceAll("[\\\\/\\?\\*\\[\\]:]", "_")
.length() > 31
? ($F{departmentId} + "_" + $F{departmentName}).replaceAll("[\\\\/\\?\\*\\[\\]:]", "_").substring(0, 31)
: ($F{departmentId} + "_" + $F{departmentName}).replaceAll("[\\\\/\\?\\*\\[\\]:]", "_")
]]>
</propertyExpression>
<!-- Freeze panes below the header table -->
<property name="net.sf.jasperreports.export.xls.freeze.row" value="3"/>
<textElement>
<font size="14" isBold="true"/>
</textElement>
<expression><![CDATA["Division: " + $F{departmentName} + " (" + $F{departmentId} + ")"]]></expression>
</element>
<!-- Column Headers -->
<element kind="staticText" x="0" y="35" width="120" height="20">
<textElement><font isBold="true"/></textElement>
<text><![CDATA[Employee Code]]></text>
</element>
<element kind="staticText" x="120" y="35" width="285" height="20">
<textElement><font isBold="true"/></textElement>
<text><![CDATA[Employee Name]]></text>
</element>
<element kind="staticText" x="405" y="35" width="150" height="20">
<textElement textAlignment="Right"><font isBold="true"/></textElement>
<text><![CDATA[Base Salary]]></text>
</element>
</band>
</groupHeader>
</group>
<detail>
<band height="20">
<element kind="textField" x="0" y="0" width="120" height="20">
<expression><![CDATA[$F{employeeCode}]]></expression>
</element>
<element kind="textField" x="120" y="0" width="285" height="20">
<expression><![CDATA[$F{employeeName}]]></expression>
</element>
<element kind="textField" x="405" y="0" width="150" height="20">
<textElement textAlignment="Right"/>
<expression><![CDATA[new java.text.DecimalFormat("#,##0.00").format($F{salary})]]></expression>
</element>
</band>
</detail>
</jasperReport>
Interactive Architecture Workspace (Split-View Studio)
Lab 4.2.1: Dynamic Group Multi-Sheet & Sheet Sanitization
Target: Excel (.xlsx)โจทย์สถาปัตยกรรม: บริษัทมีรายงานยอดขายหลายภูมิภาค (Region: North, South, East, West) คุณต้องกำหนด Group ให้เริ่มหน้าใหม่ทุกครั้งที่เปลี่ยน Region และตั้งชื่อ Sheet แต่ละ Sheet ตามชื่อ Region นั้นๆ โดยต้องแน่ใจว่าแท็กทั้งหมดเป็น JasperReports 7.0.x Unified Element Model
- ต้องประกาศ
<property name="net.sf.jasperreports.export.xls.one.page.per.sheet" value="true"/> - ต้องมี
<group name="..." isStartNewPage="true"> - ต้องใช้แท็ก
<element kind="textField" ...>เท่านั้น (ห้ามใช้แท็กโบราณ<textField>) - ต้องมี
<propertyExpression name="net.sf.jasperreports.export.xls.sheet.name">
ทดลองจำลองสถานการณ์ความผิดพลาดที่เกิดขึ้นจริงบน Production เพื่อสังเกตพฤติกรรมของ JasperReports Engine:
Production Image Specification Prompts (Visual Validation)
"2D Vector architectural diagram, modern dark slate background #0b0f19. JasperReports 7.0.3+ Engine Internal Pipeline: JRVerticalFiller streaming rows into Apache POI SXSSFWorkbook, showing isStartNewPage trigger triggering new Sheet, evaluation of net.sf.jasperreports.export.xls.sheet.name, and temp disk swap gzip files under /tmp, crisp typography, 8k resolution."
"Realistic high-end Microsoft Excel spreadsheet workbook for enterprise finance, white clean grid, tab bar at bottom showing distinct tabs: 'North_Division', 'South_Division', 'East_Division', 'West_Division'. Top rows freeze pane with corporate header, clean currency formatting $1,250,400.00, sharp sans-serif font, no blueprint outlines, production-ready."
"Realistic enterprise Excel accounting report with multiple numbered volume sheets: 'GL_Chunk_1', 'GL_Chunk_2', 'GL_Chunk_3', showing transaction logs, debit/credit balanced columns, ledger IDs, subtle alternating zebra row striping, sharp professional financial presentation."
15 Architectural Mastery Quizzes
เจาะลึกกลไก Engine, POI SXSSF, Classloading, JVM Memory และ Production Edge Cases