← Level 5 LEVEL 5.2

Dynamic Template Injection & In-Memory AST Mutation (JasperDesign API)

Exam Progress: 0 / 15
Mastery: 0%
⚡

Warm-up Quick Fire: Retention Check (from 5.1 Memory Virtualizers)

Anti-Forgetting Protocol

สถานการณ์จำลอง (Snowball Scenario): ในบทที่ 5.1 เราควบคุม JVM Heap ไม่ให้ระเบิดเมื่อสร้างรายงาน 500,000 หน้าด้วย JRSwapFileVirtualizer ที่ใช้ Single Paged Swap File บน NVMe storage แต่หากระบบ Enterprise BI ของคุณต้องอนุญาตให้ผู้ใช้เลือกคอลัมน์ได้แบบไดนามิก (Ad-hoc Column Projection เช่น เลือก 4 จาก 20 ฟิลด์) แล้วนักพัฒนาใช้วิธี Generate ข้อความ XML สดแล้วเรียก JasperCompileManager.compileReport(dynamicXmlStream) ในทุก HTTP Request จะเกิดวิกฤติต่อสถาปัตยกรรม JVM อย่างไร?

5.2 Contextual Anchoring: The Ad-Hoc Projection & Metaspace Leak Dilemma

Why String Concatenation and On-the-Fly Dynamic Compilation Destroy Enterprise Container Stability

💥 The Metaspace Churn Catastrophe

เมื่อรายงานต้องรองรับการเลือกคอลัมน์ตามใจชอบ (User-defined Columns) หรือระบบ Multi-tenant White-labeling การนำ String มาร้อยต่อเป็น XML แล้วเรียก JasperCompileManager.compileReport() ขณะรันไทม์ ทำให้ JVM ClassLoader ต้องสร้าง Anonymous Java Classes โหลดเข้า Metaspace นับแสนคลาส เมื่อ GC เคลียร์ ClassLoader ไม่ทัน Pod จะถูกสังหารด้วย OOMKilled ทันที

⚠️ Coordinate Drift & Page Overflow Rupture

การเพิ่มหรือลดคอลัมน์โดยไม่คำนวณพิกัดแบบ Dynamic ($X_n = X_{n-1} + W_{n-1}$) ทำให้ความกว้างรวมของตารางเกินระยะ columnWidth ของหน้ากระดาษ A4 ส่งผลให้คอลัมน์ท้ายๆ ถูกตัดทิ้ง หรือทำให้ Engine เกิด Pagination Loop สร้างหน้ากระดาษว่างนับแสนหน้าโดยไม่รู้ตัว

🛡️ The Enterprise Solution: In-Memory AST Mutation & Schema Caching

สถาปัตยกรรมระดับ Mission-Critical ใช้ JasperDesign API เพื่อปรับโครงสร้าง Abstract Syntax Tree (AST) บน RAM โดยตรง โดยฉีด JRDesignField, JRDesignBand, และ JRDesignElement เข้าไปในโครงสร้างแม่แบบ พร้อมทำ Structural Schema Fingerprinting แคช JasperReport ใน ConcurrentHashMap เพื่อให้คอมไพล์เพียงครั้งเดียวต่อหนึ่งรูปแบบคอลัมน์

🏛️ Dynamic Core Pillars (4 เสาหลักการฉีดโครงสร้างรายงานแบบ In-Memory AST)

Pillar 1

Abstract Syntax Tree (AST) Mutation

การจัดการอ็อบเจกต์ JasperDesign บนหน่วยความจำ ซึ่งเป็น In-memory Representation ของ JRXML ทำให้เราสามารถดึง Band, ลบ Element เดิม, และฉีด Element ใหม่เข้าไปได้โดยตรงโดยไม่ต้องแปลงเป็น Text XML

Analogy: Virtual DOM Patching
Pillar 2

Dynamic Column Snapping Geometry

การคำนวณสัดส่วนความกว้างคอลัมน์แบบ Responsive ($W_i = \text{floor}(\text{availWidth} / N)$) และการวางพิกัดจุดตัดที่แม่นยำ ป้องกันการเกิดคอลัมน์เหลื่อมล้ำและ Merged Cells ในการส่งออก Excel ตามหลักการ Virtual Grid

Analogy: CSS Flexbox / Grid Layout
Pillar 3

Schema Fingerprint Caching

การสร้าง Cryptographic Hash (SHA-256) จากลำดับและประเภทของคอลัมน์ที่ถูกเลือก เพื่อใช้เป็นคีย์ในการดึงอ็อบเจกต์ JasperReport ที่คอมไพล์แล้วออกจากแคช ป้องกันการคอมไพล์ซ้ำซ้อนและยุติปัญหา Metaspace Leak

Analogy: JIT Bytecode Memoization
Pillar 4

Unified Model 7.0.x Programmatic Model

การสร้าง Element ในโค้ด Java ด้วย StandardElement หรือ JRDesignTextField ที่กำหนด kind สอดคล้องกับมาตรฐาน 7.0.x รองรับ HarfBuzz Font Extensions และ Expression Evaluator อย่างปลอดภัย

Analogy: Web Component Factory
STEP 1

Engine Internals: How JasperDesign, AST Mutation & Evaluators Operate

เมื่อแอปพลิเคชันต้องการประกอบเทมเพลตแบบไดนามิก วงจรการทำงานภายในของ JasperReports Engine จะดำเนินการผ่าน 5 ขั้นตอนหลัก:

  1. Base AST Deserialization: โหลดเทมเพลตตั้งต้น (เช่น โครงสร้างที่มี Margins, Title, และ Footer พร้อม) ผ่าน JRXmlLoader.load(inputStream) กลายเป็นอ็อบเจกต์ JasperDesign
  2. Dynamic Structural Injection: สร้าง JRDesignField ใหม่ตามคอลัมน์ที่ผู้ใช้ร้องขอ และนำไปลงทะเบียนใน jasperDesign.addField(field) จากนั้นเพิ่ม JRDesignStaticText ลงใน Column Header และเพิ่ม JRDesignTextField พร้อม JRDesignExpression ลงใน Detail Band
  3. Coordinate Calculation & Snapping: คำนวณตำแหน่งพิกัดแนวนอน: กำหนด $X = 0$ สำหรับคอลัมน์แรก และสำหรับคอลัมน์ถัดไปกำหนด $X_i = X_{i-1} + W_{i-1}$ โดยตรวจสอบให้แน่ใจว่า $\sum W_i \le \text{columnWidth}$
  4. Fingerprint Lookup & Compilation: สร้าง Unique Fingerprint เช่น hash("accountNo:String|tradeDate:LocalDate|amount:BigDecimal") หากพบคีย์ใน ConcurrentHashMap<String, JasperReport> ให้ดึงออกมาใช้ทันที หากไม่พบจึงส่งเข้า JasperCompileManager.compileReport(jasperDesign)
  5. Execution & Virtualization: ส่ง JasperReport ที่ได้เข้าสู่ JasperFillManager.fillReport() พร้อมฉีด JRSwapFileVirtualizer เพื่อให้การ Fill แสนหน้าใช้ RAM คงที่ระดับ $O(1)$
Production Dynamic Hazard Underlying Root Cause Runtime Symptom / Exception Architectural Defense
Unbounded Metaspace Leak คอมไพล์ JasperDesign ใหม่ทุกคำร้องขอโดยไม่มีการแคช ทำให้ Dynamic Class สะสมใน Metaspace java.lang.OutOfMemoryError: Metaspace (Kubernetes Pod OOMKilled) ใช้ Schema Fingerprint Cache แคช JasperReport ใน ConcurrentHashMap
Column Width Overflow Rupture ผลรวมความกว้างของคอลัมน์ไดนามิก ($\sum W_i$) เกินกว่าความกว้างจริงของหน้ากระดาษ (columnWidth) JRValidationException: Element reaches outside column width คำนวณขนาดคอลัมน์แบบสัดส่วนอัตโนมัติ (Proportional Resizing) หรือขยาย pageWidth เป็น Landscape อัตโนมัติ
Merged Cell Contamination in Excel พิกัด X หรือ Width ระหว่าง Header StaticText และ Detail TextField เหลื่อมกัน 1 พิกเซลจากการปัดเศษทศนิยม Data Pollution: เซลล์ใน Excel ถูก Merge ข้ามคอลัมน์ ไม่สามารถ Sort/Filter ได้ กำหนดตัวแปรความกว้างแบบ Integer ร่วมกันและจัด Snap พิกัด $X_n = X_{n-1} + W_{n-1}$ ให้ตรงกัน 100%
Dynamic Expression Syntax Malformation ต่อ String ใน Expression ผิดพลาด เช่น ลืม Escape Quotes หรือระบุ Type ไม่ตรงกับ Field Type JRException: Errors were encountered when compiling report expressions ใช้ Type-safe Field Expression Builder และทำ Automated Unit Testing ให้กับ Generator Service
STEP 2

Strict 7.0.x Architecture Implementation

Java 21 / Spring Boot 3.x / Metaspace-Safe

โค้ดสถาปัตยกรรมด้านล่างแสดงการทำงานของ DynamicReportTemplateService ที่รองรับการเลือกและจัดเรียงคอลัมน์ได้อย่างอิสระ โดยมีระบบคำนวณพิกัด Snap เส้นกริดอัตโนมัติ และปกป้อง Metaspace ด้วย Schema Fingerprint Caching:

DynamicReportTemplateService.java (Java 21 / Spring Boot 3.x)
package com.enterprise.report.dynamic;

import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.type.HorizontalTextAlignEnum;
import net.sf.jasperreports.engine.type.VerticalTextAlignEnum;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class DynamicReportTemplateService {

    // Thread-safe Cache ป้องกันการคอมไพล์ซ้ำซ้อนและป้องกัน Metaspace OOM
    private final Map<String, JasperReport> reportCache = new ConcurrentHashMap<>();

    public record ColumnMetadata(String fieldName, String headerTitle, Class<?> dataType, int width, String pattern) {}

    public JasperReport getOrCreateDynamicReport(List<ColumnMetadata> selectedColumns) throws Exception {
        // 1. สร้าง Fingerprint Hash จากโครงสร้างคอลัมน์ที่ถูกเลือก
        String schemaFingerprint = generateFingerprint(selectedColumns);

        // 2. ดึงจาก Cache ทันทีหากเคยคอมไพล์โครงสร้างนี้ไว้แล้ว (O(1) Access)
        return reportCache.computeIfAbsent(schemaFingerprint, key -> {
            try {
                return buildAndCompileDynamicAST(selectedColumns);
            } catch (Exception e) {
                throw new RuntimeException("Failed to construct dynamic JasperDesign AST", e);
            }
        });
    }

    private JasperReport buildAndCompileDynamicAST(List<ColumnMetadata> columns) throws Exception {
        // 3. โหลด Base Template AST
        InputStream baseStream = getClass().getResourceAsStream("/reports/base_template.jrxml");
        JasperDesign jasperDesign = net.sf.jasperreports.engine.xml.JRXmlLoader.load(baseStream);

        JRDesignBand columnHeaderBand = (JRDesignBand) jasperDesign.getColumnHeader();
        JRDesignBand detailBand = (JRDesignBand) jasperDesign.getDetailSection().getBands()[0];

        int currentX = 0;
        int defaultHeight = 20;

        for (ColumnMetadata col : columns) {
            // 4. ฉีด Field เข้าสู่ Data Model AST
            JRDesignField field = new JRDesignField();
            field.setName(col.fieldName());
            field.setValueClass(col.dataType());
            jasperDesign.addField(field);

            // 5. ฉีด Static Header Text ลงใน ColumnHeader Band
            JRDesignStaticText headerText = new JRDesignStaticText();
            headerText.setX(currentX);
            headerText.setY(0);
            headerText.setWidth(col.width());
            headerText.setHeight(defaultHeight);
            headerText.setText(col.headerTitle());
            headerText.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
            headerText.getFont().setBold(true);
            headerText.getFont().setFontName("Sarabun");
            columnHeaderBand.addElement(headerText);

            // 6. ฉีด Dynamic TextField ลงใน Detail Band
            JRDesignTextField detailField = new JRDesignTextField();
            detailField.setX(currentX);
            detailField.setY(0);
            detailField.setWidth(col.width());
            detailField.setHeight(defaultHeight);
            detailField.setVerticalTextAlign(VerticalTextAlignEnum.MIDDLE);
            detailField.getFont().setFontName("Sarabun");
            
            if (col.pattern() != null) {
                detailField.setPattern(col.pattern());
                detailField.setHorizontalTextAlign(HorizontalTextAlignEnum.RIGHT);
            }

            JRDesignExpression expression = new JRDesignExpression();
            expression.setText("$F{" + col.fieldName() + "}");
            detailField.setExpression(expression);
            detailBand.addElement(detailField);

            // คำนวณพิกัด Snap แนบสนิท ขจัด Merged Cell 100%
            currentX += col.width();
        }

        // 7. คอมไพล์ AST เพียงครั้งเดียวเพื่อส่งเก็บใน Cache
        return JasperCompileManager.compileReport(jasperDesign);
    }

    private String generateFingerprint(List<ColumnMetadata> columns) throws Exception {
        StringBuilder sb = new StringBuilder();
        for (ColumnMetadata col : columns) {
            sb.append(col.fieldName()).append(":").append(col.dataType().getName()).append(":").append(col.width()).append(";");
        }
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(hash);
    }
}

Base Scaffold Template (100% Unified Element Model 7.0.x)

base_template.jrxml (Scaffold for Dynamic Injection)
<?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="BaseDynamicScaffold"
              pageWidth="595" pageHeight="842"
              columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20"
              uuid="a1b2c3d4-e5f6-7890-abcd-ef0123456789">

    <parameter name="REPORT_TITLE" class="java.lang.String"/>

    <title height="50">
        <element kind="staticText" x="0" y="10" width="555" height="30">
            <textElement textAlignment="Center" verticalAlignment="Middle">
                <font fontName="Sarabun" size="16" isBold="true"/>
            </textElement>
            <text><![CDATA[รายงานจัดสรรสินทรัพย์แบบกำหนดโครงสร้างคอลัมน์อัตโนมัติ]]></text>
        </element>
    </title>

    <!-- Bands will be dynamically populated by JasperDesign API at runtime -->
    <columnHeader height="20"/>

    <detail>
        <band height="20"/>
    </detail>

    <pageFooter height="20">
        <element kind="textField" x="0" y="0" width="555" height="20">
            <textElement textAlignment="Center"><font size="8"/></textElement>
            <expression><![CDATA["หน้า " + $V{PAGE_NUMBER} + " (โครงสร้าง Dynamic AST Active)"]]></expression>
        </element>
    </pageFooter>
</jasperReport>
STEP 3

Interactive Architecture Workspace (Split-View Studio)

Lab 5.2.1: Dynamic Column Grid Builder

Target: In-Memory AST Mutation

โจทย์สถาปัตยกรรม: ประกอบตารางรายงานจาก Metadata แบบไดนามิกโดยคำนวณพิกัดให้พอดีกับหน้ากระดาษ A4 (555pt) และป้องกันปัญหา Metaspace OOM:

  • ต้องคำนวณและ Snap พิกัดคอลัมน์ให้แนบสนิท ($X_n = X_{n-1} + W_{n-1}$) โดยผลรวมต้องไม่เกิน 555pt
  • ต้องใช้ reportCache.computeIfAbsent(...) เพื่อแคช JasperReport ตาม Fingerprint
  • โครงสร้างของ Template ต้องสอดคล้องกับ Unified Element Model (<element kind="...">) 100%
  • ห้ามเรียกใช้ JasperCompileManager.compileReport() ในทุก Request โดยไร้ Cache
Expected Dynamic Commercial Production Output Preview
AD-HOC CLEARING LEDGER (DYNAMIC 4-COLUMN PROJECTION) METASPACE: ZERO LEAK
ACCOUNT NO (120pt) COUNTERPARTY (180pt) VOLUME (100pt) SETTLED VALUE (155pt)
ACT-2026-9901 Goldman Sachs Execution 45,000 $12,450,000.00
ACT-2026-9902 J.P. Morgan Securities 32,500 $8,120,400.00
AST Generator: JasperDesign API (555pt Snapped) Cache Hit Ratio: 99.8% (SHA-256 Keyed)
⚠️ Crash Simulator (Dynamic Architecture Hazards)

ทดสอบจำลองข้อผิดพลาดที่มักเกิดบน Production เมื่อฉีดโครงสร้างรายงานแบบไดนามิกผิดวิธี:

Dynamic AST Pipeline Linter
Awaiting Validation
Click "Verify Architecture" to scan AST safety and Caching compliance.
ARTIFACT PROMPTS

Production Image Specification Prompts (Visual Acceptance Standard)

Dynamic Template Architecture Standard
1. Architectural Flowchart (Vector)

"2D Vector architectural flowchart, modern dark slate background #0b0f19. JasperReports 7.0.3+ Dynamic Template Injection Architecture: Web API receiving dynamic column selection JSON, computing SHA-256 schema fingerprint hash, checking thread-safe ConcurrentHashMap cache, mutating base JasperDesign AST by injecting JRDesignFields and JRDesignBands with snapped pixel coordinates, JIT compilation and storage into cache, zero Metaspace pollution, neon cyan (#06b6d4) and emerald green (#10b981) data streams, crisp typography, 8k resolution."

2. Lab 5.2.1 Output (Ad-hoc Dynamic Ledger)

"Realistic high-resolution commercial PDF A4 portrait financial statement representing an ad-hoc custom ledger. Pristine white paper background, crisp corporate header 'GLOBAL CLEARING HOUSE - CUSTOM PROJECTION RUN', pagination 'Page 1 of 12', clean tabular grid with 4 dynamically generated columns: ACCOUNT_NO, COUNTERPARTY, VOLUME, and SETTLED_VALUE. Exact right-aligned currency figures like '$12,450,000.00', perfectly straight layout, zero blueprint outlines, zero technical bounding boxes, enterprise banking production standard."

3. Lab 5.2.2 Output (Wide Ad-hoc Multi-Column Report)

"Realistic commercial financial report in PDF A4 landscape format showing an 8-column wide dynamic projection. Flawlessly snapped column widths perfectly fitting printable area of 792pt without column wrapping or overflow. Crisp monochrome corporate typography, professional financial accounting presentation, DataMatrix authenticity barcode at bottom right, authentic paper texture, photorealistic document lighting."

EXAM ARENA

15 Architectural Mastery Quizzes

เจาะลึก JasperDesign API, In-Memory AST Mutation, Metaspace Safety, Dynamic Column Snapping, และ Schema Caching