Level 3: Application Integration • Chapter 3.2 JVM Runtime APIs & Streaming Architecture
คำถาม: ในกระบวนการ CI/CD Build Pipeline ของบทที่ 3.1 เหตุใดการกำหนดให้อาร์ติแฟกต์รายงานถูกบันทึกเป็น .jasper (Binary Bytecode) ลงใน target/classes/reports/ จึงทำให้ Production Microservice บน Java 21 ประหยัด CPU และ Metaspace ได้อย่างเด็ดขาด?
.jasper คือ Java Serialized Object ของคลาส JasperReport ที่คอมไพล์ Expression Evaluators เป็น Bytecode เรียบร้อยแล้ว ขณะรันไทม์ Service เพียงแค่ทำ In-memory Deserialization ผ่าน JRLoader.loadObject(...) ในเวลาไม่เกิน 5ms โดยไม่มีการเรียก Compiler Toolchain หรือสร้าง Dynamic Classes โหลดเข้า JVM Metaspace เลยแม้แต่น้อย
The "Why" & Real-world Problem: The ByteArrayOutputStream Memory Blast
ในระบบ REST API บน Spring Boot ที่ผู้ใช้ดาวน์โหลดรายงานพร้อมกัน 100 concurrent requests ข้อผิดพลาดที่พบบ่อยที่สุดคือการใช้ ByteArrayOutputStream เพื่อเรนเดอร์ PDF ทั้งไฟล์เก็บไว้ในหน่วยความจำ RAM ก่อนส่งกลับเป็น byte[]:
// ❌ CRITICAL ANTI-PATTERN: Heap Explosion ByteArrayOutputStream baos = new ByteArrayOutputStream(); JasperExportManager.exportReportToPdfStream(jasperPrint, baos); byte[] pdfBytes = baos.toByteArray(); // หากเอกสารมีขนาด 80MB x 100 requests = 8GB Heap Allocation Churn ทันที! return ResponseEntity.ok().body(pdfBytes);
สถาปัตยกรรมระดับ Enterprise กำหนดให้ใช้ Non-blocking Streaming API โดยไปป์ข้อมูลจาก Exporter ตรงสู่ Response OutputStream ทันทีผ่าน StreamingResponseBody หรือ Jakarta Servlet Stream ร่วมกับ Report Template Caching ข้ามเธรด
JasperReport เป็น Read-Only Object ที่มีความปลอดภัยระดับ Thread-Safe 100% ต้องโหลดขึ้นหน่วยความจำเพียงครั้งเดียวผ่าน ConcurrentHashMap แล้วแชร์ให้ทุก Worker Thread ในการ Fill โดยห้าม Reload ซ้ำ
ส่งมอบ JRPdfExporter ตรงสู่ HttpServletResponse.getOutputStream() ผ่าน Spring Boot StreamingResponseBody โดยไม่มี Intermediate Byte Array Buffering ลด Heap footprint ลงเหลือเกือบ 0
Map<String, Object> parameters ต้องถูกสร้างใหม่แบบ Isolate ต่อ 1 Request ห้ามใช้ Shared Static Map เด็ดขาด เพราะ Engine มีการ Inject Built-in Environment Keys (เช่น REPORT_PARAMETERS_MAP) ขณะ Fill
JasperReports 7.0.x ใช้ SimplePdfExporterConfiguration และ SimplePdfReportConfiguration ในการควบคุมการบีบอัด Metadata, Version, และ Chunked Encoding
| Failure Pattern | Engine & JVM Mechanics | Architectural Defense (Standard 7.0.x) |
|---|---|---|
| ByteArray Buffering Churn | การสร้าง byte[] ขนาดใหญ่ทำให้เกิด Garbage Collector Young Gen Exhaustion และ Full GC Stop-the-World ยาวนาน |
ใช้ Spring Boot StreamingResponseBody หรือ Jakarta Servlet getOutputStream() เชื่อมต่อตรงกับ Exporter |
| Template Reloading Overhead | โหลดไฟล์ .jasper จากดิสก์ใหม่ทุก Request ทำให้ Disk I/O Bottleneck และสูญเสีย CPU ในการ Deserialize ซ้ำๆ |
ทำ In-Memory Concurrent Cache เก็บ Pre-compiled JasperReport อ็อบเจกต์ระดับ Application Context |
| Parameter Mutation Contention | การแชร์ Parameter Map ข้าม Thread ทำให้ค่าตัวแปรในรายงานตีกัน (Race Condition & Data Leak) | สร้าง HashMap ตัวใหม่ต่อ 1 การเรียกใช้งาน JasperFillManager.fillReport() เสมอ |
สลับแท็บเพื่อศึกษาและทดสอบทั้ง 2 ส่วน: Lab 3.2.1 (Spring Boot 3.x Streaming Service) และ Lab 3.2.2 (Template Specification 7.0.x)
| SYMBOL | SECURITY DESCRIPTION | UNITS | PRICE | NET VALUE |
|---|---|---|---|---|
| NVDA | NVIDIA CORP COMMON STOCK | 5,000 | $124.50 | $622,500.00 |
| AAPL | APPLE INC COMMON STOCK | 4,200 | $228.10 | $958,020.00 |
| MSFT | MICROSOFT CORP COMMON STOCK | 3,100 | $442.80 | $1,372,680.00 |
| AMZN | AMAZON COM INC COMMON STOCK | 6,500 | $186.40 | $1,211,600.00 |
ทดสอบความเข้าใจเชิงลึกเกี่ยวกับ Java 21 Streaming, Exporter Configuration, Heap Allocation และ Garbage Collection
@RestController
@RequestMapping("/api/v1/reports")
public class SettlementReportController {
private final SettlementStreamingReportService reportService;
public SettlementReportController(SettlementStreamingReportService reportService) {
this.reportService = reportService;
}
@GetMapping(value = "/settlement-statement", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<StreamingResponseBody> streamSettlementPdf(
@RequestParam("statementId") String statementId) {
StreamingResponseBody responseBody = outputStream -> {
reportService.streamSettlementPdf(statementId, outputStream);
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"settlement-" + statementId + ".pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(responseBody);
}
}