Warm-up Quick Fire: Retention Check (from 4.3 PDF/A & Cryptography)
สถานการณ์จำลอง (Snowball Scenario): หากระบบของคุณต้องส่งออกเอกสารใบเสร็จตามมาตรฐาน ISO 19005-1 (PDF/A-1a) เพื่อส่งเข้าคลังเอกสารระยะยาว แต่ทีม Security ร้องขอให้ใส่ Password Encryption (User/Owner Password) เพื่อป้องกันไม่ให้บุคคลภายนอกเปิดดู คุณในฐานะ Enterprise Architect ควรตัดสินใจและแก้ปัญหาอย่างไรจึงจะถูกต้องตามสเปกสากล?
4.4 Contextual Anchoring: The Multi-Format Matrix & Headless Streaming Bottleneck
Why Single-Template Multi-Format Dispatching Fails in High-Throughput Microservice Architectures
ในสถาปัตยกรรม Web API ทั่วไป นักพัฒนามักส่งออกรายงานผ่าน ByteArrayOutputStream ก่อนจะแปลงเป็น byte[] ส่งกลับผ่าน Spring Web ResponseEntity<byte[]> หากมีคำขอส่งออกรายงานขนาด 200MB พร้อมกัน 20 เธรด JVM Heap จะถูกจองทันที 8GB ส่งผลให้เกิด Stop-The-World GC และ Crash ทั้ง Pod
เมื่อรายงานธุรกิจต้องรองรับทั้งการพิมพ์หน้ากระดาษ (PDF), การคำนวณสูตร (XLSX), และการสกัดข้อมูลดิบเข้า Data Lake (CSV/JSON) หากไม่มีระบบ ExporterFilter ตัว Exporter จะนำโลโก้รูปภาพ, ข้อความ Disclaimer, หมายเลขหน้ากระดาษ, และเส้นตาราง มาปะปนในไฟล์ CSV จนกลายเป็นขยะข้อมูลที่ระบบ ETL ไม่สามารถนำไปประมวลผลต่อได้
JasperReports 7.0.x นำเสนอสถาปัตยกรรม JRAbstractExporter Pipeline ร่วมกับ JROriginExporterFilter และ SimpleOutputStreamExporterOutput ที่ผูกเข้ากับ HttpServletResponse.getOutputStream() ได้โดยตรง ทำให้สตรีมไบต์เอกสารแบบ Real-time Chunked Transfer Encoding สู่ Client โดยมี Heap Overhead ใกล้ศูนย์
🏛️ Dynamic Core Pillars (4 เสาหลักระบบส่งออกสากลแบบ Headless)
Universal Exporter Factory
การออกแบบ Factory Pattern ควบคุมวงจรชีวิตของ Exporter แต่ละตระกูล (JRPdfExporter, JRXlsxExporter, JRCsvExporter, HtmlExporter) จากอินพุต JasperPrint ชุดเดียวกัน พร้อมตั้งค่า Context Classloader และ Metadata สอดคล้องกับ MIME Types
Granular Origin Filtering
การคัดกรอง Band และ Element ด้วย net.sf.jasperreports.export.{format}.exclude.origin.* เพื่อลบ Header/Footer, ภาพตกแต่ง และแท็กกราฟิกออกจากการส่งออก CSV/XLSX โดยคงความสมบูรณ์ 100% ไว้ใน PDF
Direct HTTP Socket Flushing
การสตรีมผ่าน Socket Stream โดยตรง ขจัด Intermediary In-memory Byte Arrays พร้อมจัดการ HTTP Header: Transfer-Encoding: chunked, Content-Disposition: attachment และการดักจับ Client Abort Exceptions (Broken Pipe)
RFC 4180 CSV & Delimiter Sanitization
การกำหนดค่า fieldDelimiter, recordDelimiter, และ Character Encoding (UTF-8 with BOM หรือ UTF-8 Plain) ป้องกันอักขระลูกน้ำ แท็บ หรือ Newline ในข้อมูลทำให้โครงสร้างแถวแตก
Engine Internals: JRAbstractExporter Lifecycle & ExporterFilter Pipelines
เมื่อคำสั่ง exporter.exportReport() ถูกเรียก ตัว Exporter ซึ่งสืบทอดมาจากคลาสแม่ JRAbstractExporter จะดำเนินการตามวงจรชีวิต 5 ขั้นตอน:
- Input Resolution & State Validation: ดึงอ็อบเจกต์
JasperPrintตรวจสอบหน้ากระดาษ (getPages()) และเริ่มต้นสร้าง Exporter Context - ExporterFilter Evaluation: ก่อนที่ Element ใดๆ จะถูกวาดหรือเขียนลง Stream ตัว Engine จะส่ง Element นั้นไปทดสอบกับ
ExporterFilter.isToExport(JRPrintElement)หาก Return เป็นfalseElement นั้นจะถูกข้ามทันทีโดยไม่เสีย CPU Cycle - Grid Mapping vs Absolute Coordinate Layout: หากเป็น Exporter ตระกูล Text/Grid (XLSX, CSV, HTML) Engine จะส่งข้อมูลเข้าสู่ JRAbstractGridEvaluator เพื่อตัด Virtual Grid Lines แต่หากเป็น Page-based (PDF) จะรักษาพิกัดเวกเตอร์ X, Y, Width, Height ไว้ตรงเป๊ะ
- Progressive Chunk Flushing: สำหรับ
JRCsvExporterไบต์ข้อความจะถูกสตรีมลงWriterทันทีทีละ Record ซึ่งช่วยลดหน่วยความจำลงเหลือระดับคงที่ $O(1)$ - Stream Finalization & Cleanup: สั่ง Flush บัฟเฟอร์ ปิด Stream และปล่อยตัวจัดการไฟล์ชั่วคราวทิ้ง
| Production Streaming Hazard | Underlying Root Cause | Runtime Symptom / Exception | Architectural Defense |
|---|---|---|---|
| Client Disconnect Broken Pipe | ผู้ใช้งานกด Cancel Download หรือปิดหน้าต่างเบราว์เซอร์ระหว่างสตรีมไฟล์ขนาดใหญ่ ทำให้ TCP Socket ปลายทางถูกตัด | ClientAbortException: java.io.IOException: Broken pipe | ดักจับ ClientAbortException ใน Controller Layer และปิด Stream เงียบๆ โดยไม่บันทึก Error Log ระดับ FATAL |
| CSV Delimiter Escaping Crash | ข้อมูลในฟิลด์มีเครื่องหมายจุลภาค (,) หรือเครื่องหมายคำพูด (") ซ้อนอยู่ แต่ Exporter ไม่ได้ตั้ง Quote Qualifier |
Data Pipeline Corruption: แถวข้อมูลเลื่อนคอลัมน์ (Column Shift) ในระบบ ETL | ตั้งค่า net.sf.jasperreports.export.csv.field.delimiter และเปิด force.field.enclosure=true |
| Heap Exhaustion via byte[] Response | ใช้ ByteArrayOutputStream เก็บเอกสารทั้งหมดในหน่วยความจำ RAM ก่อนส่งกลับ |
OutOfMemoryError: Java heap space ในเธรด Tomcat/Undertow | ส่งผ่าน response.getOutputStream() เข้าสู่ Exporter Output โดยตรง พร้อมตั้ง Header chunked |
| Thai CSV Mojibake in Microsoft Excel | ส่งออก CSV เป็น UTF-8 แต่ไม่ได้ใส่ UTF-8 Byte Order Mark (BOM: 0xEF, 0xBB, 0xBF) ที่หัวไฟล์ |
ภาษาไทยกลายเป็นอักษรต่างดาว (เธเธฒเธฉเธฒเนเธ—เธข) เมื่อดับเบิลคลิกเปิดใน Excel | เขียนไบต์ BOM 3 ไบต์แรกนำหน้าลงใน OutputStream หรือตั้งค่า characterEncoding="UTF-8" พร้อม BOM flag |
Strict 7.0.x Architecture Implementation
เทมเพลต JRXML ด้านล่างแสดงการกำหนดคุณสมบัติการคัดกรองข้ามฟอร์แมต (Origin Filtering) เพื่อให้ Band ตกแต่งถูกกรองทิ้งอัตโนมัติใน CSV/XLSX ขณะที่ยังคงแสดงผลสมบูรณ์ใน PDF โดยใช้แท็กสมัยใหม่ <element kind="..."> 100%:
<?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="UniversalMultiFormatPipeline"
pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20"
uuid="a81b2c3d-4e5f-6789-0123-abcdef456789">
<!-- ================================================================= -->
<!-- Architectural Origin Filters: Exclude Page Decorators in CSV/XLSX -->
<!-- ================================================================= -->
<property name="net.sf.jasperreports.export.csv.exclude.origin.band.1" value="pageHeader"/>
<property name="net.sf.jasperreports.export.csv.exclude.origin.band.2" value="pageFooter"/>
<property name="net.sf.jasperreports.export.csv.exclude.origin.band.3" value="lastPageFooter"/>
<property name="net.sf.jasperreports.export.xls.exclude.origin.band.1" value="pageHeader"/>
<property name="net.sf.jasperreports.export.xls.exclude.origin.band.2" value="pageFooter"/>
<!-- CSV Formatting Standards (RFC 4180) -->
<property name="net.sf.jasperreports.export.csv.field.delimiter" value=","/>
<property name="net.sf.jasperreports.export.csv.record.delimiter" value="
"/>
<parameter name="REPORT_TITLE" class="java.lang.String"/>
<parameter name="EXPORTED_BY" class="java.lang.String"/>
<field name="transactionId" class="java.lang.String"/>
<field name="customerCode" class="java.lang.String"/>
<field name="settlementAmount" class="java.math.BigDecimal"/>
<field name="transactionDate" class="java.time.LocalDate"/>
<title height="50">
<element kind="staticText" x="0" y="0" width="555" height="25">
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="Sarabun" size="16" isBold="true"/>
</textElement>
<text><![CDATA[รายงานสรุปธุรกรรมการเงินสากล (Universal Clearing Ledger)]]></text>
</element>
<element kind="textField" x="0" y="25" width="555" height="20">
<textElement textAlignment="Center">
<font fontName="Sarabun" size="10"/>
</textElement>
<expression><![CDATA["จัดทำโดย: " + $P{EXPORTED_BY}]]></expression>
</element>
</title>
<columnHeader height="20">
<element kind="staticText" x="0" y="0" width="120" height="20">
<textElement><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[TXN_ID]]></text>
</element>
<element kind="staticText" x="120" y="0" width="180" height="20">
<textElement><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[CUSTOMER_CODE]]></text>
</element>
<element kind="staticText" x="300" y="0" width="135" height="20">
<textElement textAlignment="Right"><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[AMOUNT]]></text>
</element>
<element kind="staticText" x="435" y="0" width="120" height="20">
<textElement textAlignment="Right"><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[TXN_DATE]]></text>
</element>
</columnHeader>
<detail>
<band height="20">
<element kind="textField" x="0" y="0" width="120" height="20">
<expression><![CDATA[$F{transactionId}]]></expression>
</element>
<element kind="textField" x="120" y="0" width="180" height="20">
<expression><![CDATA[$F{customerCode}]]></expression>
</element>
<element kind="textField" x="300" y="0" width="135" height="20" pattern="#,##0.00">
<textElement textAlignment="Right"/>
<expression><![CDATA[$F{settlementAmount}]]></expression>
</element>
<element kind="textField" x="435" y="0" width="120" height="20">
<textElement textAlignment="Right"/>
<expression><![CDATA[$F{transactionDate}.toString()]]></expression>
</element>
</band>
</detail>
<pageFooter height="20">
<!-- Page Footer will be automatically suppressed in CSV/XLSX via Exclude Properties -->
<element kind="textField" x="0" y="0" width="555" height="20">
<textElement textAlignment="Center"><font size="8"/></textElement>
<expression><![CDATA["หน้า " + $V{PAGE_NUMBER}]]></expression>
</element>
</pageFooter>
</jasperReport>
Enterprise Headless Streaming Controller (Java 21 / Spring Boot 3.x)
package com.enterprise.report.controller;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.export.SimpleWriterExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import net.sf.jasperreports.csv.JRCsvExporter;
import net.sf.jasperreports.csv.SimpleCsvExporterConfiguration;
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
import org.springframework.web.bind.annotation.*;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/api/v1/reports")
public class HeadlessReportStreamController {
private final ReportExecutionService executionService;
public HeadlessReportStreamController(ReportExecutionService executionService) {
this.executionService = executionService;
}
@GetMapping("/stream")
public void streamReport(
@RequestParam String format,
HttpServletResponse response) throws Exception {
JasperPrint jasperPrint = executionService.generatePrintObject();
OutputStream out = response.getOutputStream();
switch (format.toLowerCase()) {
case "pdf" -> {
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=\"clearing_ledger.pdf\"");
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
exporter.exportReport();
}
case "csv" -> {
response.setContentType("text/csv; charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"clearing_ledger.csv\"");
// Write UTF-8 BOM so Microsoft Excel renders Thai text properly
out.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});
JRCsvExporter exporter = new JRCsvExporter();
exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
exporter.setExporterOutput(new SimpleWriterExporterOutput(out, StandardCharsets.UTF_8.name()));
SimpleCsvExporterConfiguration config = new SimpleCsvExporterConfiguration();
config.setFieldDelimiter(",");
config.setRecordDelimiter("\n");
exporter.setConfiguration(config);
exporter.exportReport();
}
case "xlsx" -> {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"clearing_ledger.xlsx\"");
JRXlsxExporter exporter = new JRXlsxExporter();
exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
exporter.exportReport();
}
default -> throw new IllegalArgumentException("Unsupported format: " + format);
}
out.flush();
}
}
Interactive Architecture Workspace (Split-View Studio)
Lab 4.4.1: Dual-Target Origin Filtering (PDF vs Excel/CSV)
Target: Clean Dual Export
โจทย์สถาปัตยกรรม: ป้องกันไม่ให้องค์ประกอบของหัวกระดาษ (pageHeader) และท้ายกระดาษ (pageFooter) หลุดเข้าไปในไฟล์ CSV และ Excel โดยต้องรักษาโครงสร้างหน้ากระดาษไว้เต็มรูปแบบใน PDF:
- ต้องประกาศคุณสมบัติ
net.sf.jasperreports.export.csv.exclude.origin.band.* - ต้องประกาศคุณสมบัติ
net.sf.jasperreports.export.xls.exclude.origin.band.* - ทุก Visual Element ต้องใช้แท็ก Unified Element Model (
<element kind="...">) 100% - ต้องระบุ
fieldDelimiterให้ได้มาตรฐาน RFC 4180
| TXN | AMOUNT |
|---|---|
| TX-901 | $450,000.00 |
ทดสอบจำลองข้อผิดพลาดที่มักเกิดบน Production เมื่อสตรีมรายงานแบบ Headless บนระบบ Microservices:
Production Image Specification Prompts (Visual Acceptance Standard)
"2D Vector architectural flowchart, modern dark slate background #0b0f19. JasperReports 7.0.3+ Universal Headless Streaming Pipeline: JasperPrint being dispatched through JRAbstractExporter, evaluated by ExporterFilter, splitting into JRPdfExporter, JRXlsxExporter, and JRCsvExporter, streaming directly into HttpServletResponse OutputStream with chunked transfer encoding, neon cyan and emerald data streams, crisp typography, 8k resolution."
"Side-by-side realistic enterprise production view: Left side showing a pristine white A4 PDF financial ledger with corporate header, pagination 'Page 1 of 5', subtle blue border. Right side showing unpolluted raw CSV text output with clean comma delimiters and zero page headers, perfect monetary formatting, sharp typography, zero blueprint lines, production-ready."
"Macro photography of a realistic financial terminal screen displaying an RFC 4180 compliant CSV stream import. Clear columns for TRANSACTION_REF, COUNTERPARTY, VALUE_USD, VALUE_DATE, properly quoted fields containing commas, razor-sharp monospaced font, emerald-on-black data visualization theme, commercial banking standard."
15 Architectural Mastery Quizzes
เจาะลึก JRAbstractExporter, ExporterFilter, Non-blocking Streaming, RFC 4180 CSV, และ Jakarta EE HTTP Pipeline