ในสเตจ Fill Stage เครื่องยนต์ JRVerticalFiller ทำงานประมวลผลข้อมูลในลักษณะ Streaming Single-Pass ผ่าน JRDataSource แต่เมื่อพบองค์ประกอบที่ต้องการผลลัพธ์ล่วงหน้า (เช่น ยอดรวมสุทธิบน Title Band หรือเลขหน้า "Page X of Y") เครื่องยนต์จะเปิดใช้งานกลไก Two-Pass Delayed Evaluation โดยหน่วงเวลาการคำนวณและกักเก็บ Reference ลงใน Lookahead Fill Buffer จนกระทั่งถึงจุดสิ้นสุดของ Scope ที่กำหนด
เมทริกซ์การทำงานของ evaluationTime:
| ค่า evaluationTime | จังหวะเวลาการประเมินผลค่า (Timing) |
|---|---|
Now (Default) |
ประเมินผลทันที ณ เสี้ยววินาทีที่ Band นั้นถูกวาดลงบน Canvas |
Report |
หน่วงเวลาประเมินผลจนกว่า DataSource จะวิ่งจบ Record สุดท้ายของเอกสาร |
Page |
หน่วงเวลาประเมินผลจนกระทั่งจบหน้าปัจจุบัน (ก่อนวาด Page Footer) |
Column |
หน่วงเวลาประเมินผลจนกว่าจะจบคอลัมน์ปัจจุบันในรายงานแบบ Multi-Column |
Group |
หน่วงเวลาประเมินผลจนกระทั่ง Group สิ้นสุดลง (พร้อม Group Footer) |
Band |
ประเมินผลหลังจากทุก Element ภายใน Band นั้นยืดขยายตัวเสร็จสิ้น |
Auto |
ประเมินผลแบบผสมอัตโนมัติ (เช่น Text รวม Field กับ Variable ที่มีจังหวะต่างกัน) |
การควบคุม Variable Calculation & Reset Scope:
calculation="Sum": สะสมผลบวกอัตโนมัติ ควบคู่กับ initialValueExpression เพื่อป้องกันค่าเริ่มต้นเป็น nullresetType="Report": สะสมต่อเนื่องตลอดเอกสาร (ไม่ Reset ค่ากลับ)resetType="None": รับค่า Expression ล่าสุดของแต่ละ Record โดยตรงโดยไม่ผ่านฟังก์ชันสะสมevaluationTime="Report" กับ Elements จำนวนมหาศาลบนรายงานระดับแสนแถว JVM จะต้องตรึง Object References ไว้ใน Heap Memory โดยไม่ยอมให้ Garbage Collector เก็บกวาด จนกว่าเอกสารจะ Fill เสร็จสมบูรณ์ ส่งผลให้ Heap บวมและเกิด OutOfMemoryError ได้ง่ายขึ้น<field class="..."> ใน JRXML ไม่ตรงกับ Java Class จริงที่ DataSource ฉีดเข้ามา JVM 17/21 จะ Crash ทันทีระหว่าง Runtime
<!-- JRXML 7.0.x Schema: Two-Pass & Type-Safe Pipeline -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
name="TwoPassInvoice" pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20"
topMargin="20" bottomMargin="20"
whenNoDataType="AllSectionsNoDetail">
<!-- Ingestion Pipeline: ประกาศ Explicit Java Types -->
<field name="item_name" class="java.lang.String"/>
<field name="unit_price" class="java.math.BigDecimal"/>
<field name="quantity" class="java.lang.Integer"/>
<field name="discount_rate" class="java.lang.Double"/>
<!-- Variable: สะสมยอดรวมทั้งฉบับ -->
<variable name="TOTAL_NET" calculation="Sum" resetType="Report"
class="java.math.BigDecimal">
<expression><![CDATA[
$F{unit_price}.multiply(new java.math.BigDecimal($F{quantity}))
.multiply(java.math.BigDecimal.valueOf(1.0 - ($F{discount_rate} != null ? $F{discount_rate}.doubleValue() : 0.0)))
]]></expression>
<initialValueExpression><![CDATA[java.math.BigDecimal.ZERO]]></initialValueExpression>
</variable>
<title>
<band height="60">
<!-- ยอดรวมทั้งฉบับแสดงบนหน้าแรกทันทีผ่าน evaluationTime="Report" -->
<element kind="textField" x="355" y="15" width="200" height="25"
evaluationTime="Report" pattern="#,##0.00" blankWhenNull="true">
<expression><![CDATA[$V{TOTAL_NET}]]></expression>
</element>
</band>
</title>
<pageFooter>
<band height="25">
<!-- สถาปัตยกรรม Page X of Y สองจังหวะ -->
<element kind="textField" x="380" y="5" width="80" height="18" evaluationTime="Now">
<expression><![CDATA["Page " + $V{PAGE_NUMBER} + " of "]]></expression>
</element>
<element kind="textField" x="460" y="5" width="50" height="18" evaluationTime="Report">
<expression><![CDATA[$V{PAGE_NUMBER}]]></expression>
</element>
</band>
</pageFooter>
</jasperReport>
กลไกเบื้องหลัง:
• pattern="#,##0.00": จัดรูปแบบตัวเลขทศนิยมทางการเงิน 2 ตำแหน่งอย่างเป็นทางการ
• blankWhenNull="true": ดักจับค่า null ไม่ให้พิมพ์ข้อความ "null" หรือเกิด NPE กลางคัน
เงื่อนไขทางเทคนิค (Full-Coverage 100%):
1. กำหนดตัวแปรสะสมยอดเงินรวม <variable name="TOTAL_AMOUNT" ...> โดยระบุ calculation="Sum" และ resetType="Report"
2. กำหนด initialValueExpression ของตัวแปรเป็น BigDecimal.ZERO ป้องกันค่า null เริ่มต้น
3. วาง TextField แสดงยอดรวมใน <title> Band โดยระบุ evaluationTime="Report" เพื่อดึงยอดรวมข้ามหน้ามาแสดงที่หัวกระดาษ
4. ออกแบบโครงสร้างเลขหน้า "Page X of Y" ใน <pageFooter> โดยกล่องแรกมี evaluationTime="Now" และกล่องหลังมี evaluationTime="Report"
5. กำหนด pattern="#,##0.00" สำหรับตัวเลขยอดเงิน และใช้ไวยากรณ์ 7.0.x Unified Model (<element kind="textField">)
เงื่อนไขทางเทคนิค (Full-Coverage 100%):
1. กำหนด Class Types ของฟิลด์ให้ถูกต้องตามข้อกำหนดการเงิน: BigDecimal สำหรับ unit_price, Integer สำหรับ quantity, และ Double สำหรับ discount_rate
2. ป้องกัน ClassCastException และ Precision Loss โดยใช้เมธอด .multiply() บน BigDecimal แทนเครื่องหมายคูณ *
3. สร้าง Ternary Operator ป้องกัน NullPointerException บนฟิลด์ส่วนลด: ($F{discount_rate} != null ? $F{discount_rate}.doubleValue() : 0.0)
4. ใส่ blankWhenNull="true" บนทุก TextField ที่ทำการแสดงผลข้อมูล
5. กำหนด pattern="#,##0.00" เพื่อให้การแสดงผลทศนิยมทางการเงินสมบูรณ์แบบ
แบบทดสอบ 15 ข้อเชิงลึก อ้างอิงจากหลักการ Data Pipeline, Evaluation Timing และการจัดการหน่วยความจำบน Production: