Apache POI OOXML – Fat-Jar deploy issue

Apache POI OOXML – Fat-Jar deploy issue

ที่เป็นอย่างนี้ เพราะมีการใช้ WorkbookFactory
แต่ WorkbookFactory มันก็น่าใช้ เพราะมันช่วยจัดการไฟล์ทั้ง XLS และ XLSX ได้โดยเราไม่ต้องเขียน code 2 รอบ

OLE2: คือรูปแบบของไฟล์ Microsoft Office รุ่นเก่า (Excel 97-2003) ซึ่งก็คือไฟล์นามสกุล .xls
OOXML: คือรูปแบบของไฟล์ Microsoft Office รุ่นใหม่ๆ (Excel 2007 ขึ้นไป) ซึ่งก็คือไฟล์นามสกุล .xlsx

Apache POI แยก library สำหรับการอ่านไฟล์ .xls และ .xlsx ออกจากกัน
ในการอ่านไฟล์ .xlsx (OOXML) ต้องใช้ poi-ooxml-*.jar
ในการอ่านไฟล์ .xls (OLE2) ต้องใช้ poi-*.jar

ข้อความ FileMagic: OLE2 ที่แสดงขึ้นมาเป็นตัวบ่งชี้ที่สำคัญว่า Apache POI ตรวจพบว่าไฟล์นี้น่าจะเป็นไฟล์ Excel .xls (รุ่นเก่า) แต่ไม่สามารถอ่านโครงสร้างภายในไฟล์ได้ถูกต้อง จึงเกิด error ขึ้น

แต่ตอน deploy เป็น Fat-Jar ดันเกิดปัญหา ซึ่ง AI วิเคราะห์ว่าน่าจะเป็นเพราะการทำ package มันเจอ services ที่ซ้ำซ้อนกัน แล้วมันเลือกมาแค่อันเดียว

ก่อนอื่น ยังงัยก็ต้องตรวจสอบว่า ไฟล์ Apache POI / OOXML ถูกรวมไว้ใน Fat-Jar แล้วหรือยัง
นอกจากนั้นก็ลองตรวจสอบว่าไฟล์ XML มีปัญหาหรือไม่
ทำงานไม่ได้ทั้ง XLS และ XLSX หรือเปล่า

## สาเหตุที่แท้จริง: การรวมไฟล์ META-INF/services ทับกัน

อธิบายง่ายๆ คือ:

  • ทั้ง poi.jar (สำหรับ .xls) และ poi-ooxml.jar (สำหรับ .xlsx) ต่างก็มีไฟล์พิเศษที่ใช้สำหรับ “ลงทะเบียน” ตัวเองกับ WorkbookFactory ไฟล์นี้จะอยู่ใน META-INF/services/
  • เมื่อ Gradle Shadow Plugin สร้าง Fat JAR มันจะเจอฟაイルชื่อเดียวกันจาก 2 ที่นี้
  • โดยปกติแล้ว มันจะเลือกเอามาแค่ไฟล์เดียว และทิ้งอีกไฟล์ไป!

ผลก็คือ Fat JAR ของคุณมีคลาสสำหรับ .xls และ .xlsx ครบถ้วน แต่ไฟล์ทะเบียน (Service file) มีแค่ของ .xlsx อันเดียว ทำให้ WorkbookFactory รู้จักแต่วิธีเปิด .xlsx เท่านั้น ซึ่งตรงกับ Error ที่ฟ้องว่า having providers: [XSSF...] เป๊ะๆ เลยครับ

## วิธีแก้ไขใน Gradle (build.gradle)

เราต้องสั่งให้ Shadow Plugin “รวม” (Merge) เนื้อหาของไฟล์ทะเบียนนี้เข้าด้วยกัน แทนที่จะเลือกมาแค่อันเดียว

ให้คุณเพิ่มการตั้งค่าสำหรับ shadowJar task ในไฟล์ build.gradle ของคุณดังนี้ครับ

// ... ส่วน plugins และ dependencies ของคุณ ...

// เพิ่ม task นี้เข้าไป หรือแก้ไขของเดิมถ้ามีอยู่แล้ว
shadowJar {
    // คำสั่งนี้คือพระเอกของงานครับ
    mergeServiceFiles()
    
    // ถ้าคุณมีการตั้งค่าอื่นๆ ก็ใส่ไว้ตามเดิม
    archiveBaseName = 'polar_gce'
    archiveClassifier = 'all'
    archiveVersion = '1.0'
}

// ถ้าคุณใช้ application plugin ให้แน่ใจว่าได้กำหนด main class ไว้
application {
    mainClass = 'com.ncs2polar.app.SpeedStackTableFrame'
}

สิ่งที่ต้องทำ:

  1. นำ shadowJar { mergeServiceFiles() } ไปใส่ใน build.gradle
  2. รันคำสั่ง build ใหม่อีกครั้ง: ./gradlew clean shadowJar
  3. นำไฟล์ JAR ที่ได้จาก build/libs ไปทดสอบอีกครั้ง

การ MergeServiceFiles ช่วยแก้ปัญหาได้จริง

แต่ AI ยังแนะนำวิธีที่ 2 ให้ ซึ่งก็น่าจะช่วยแหล่ะ แต่มันดูไม่สะดวก คือ การไม่ใช้ WorkbookFactory

## ถ้ายังไม่ได้: ลองสร้าง Workbook โดยตรง (วิธีสำรอง)

หากวิธีข้างบนยังไม่ได้ผล (ซึ่งไม่น่าเป็นไปได้) ให้ลองใช้วิธีสุดท้ายคือเลิกใช้ WorkbookFactory แล้วเขียนโค้ดเพื่อสร้าง Workbook เองโดยตรง วิธีนี้เป็นการแก้ปัญหาที่ปลายเหตุ แต่ทำให้โปรแกรมทำงานได้แน่นอน

import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; // for .xls
import org.apache.poi.xssf.usermodel.XSSFWorkbook; // for .xlsx
import java.io.FileInputStream;
import java.io.InputStream;

// ...

String filePath = "path/to/your/file.xls";
Workbook workbook = null;
InputStream inputStream = new FileInputStream(filePath);

try {
    if (filePath.toLowerCase().endsWith(".xls")) {
        // สร้าง Workbook สำหรับ .xls โดยตรง
        workbook = new HSSFWorkbook(inputStream);
    } else if (filePath.toLowerCase().endsWith(".xlsx")) {
        // สร้าง Workbook สำหรับ .xlsx โดยตรง
        workbook = new XSSFWorkbook(inputStream);
    } else {
        throw new IllegalArgumentException("Invalid file extension. Only .xls and .xlsx are supported.");
    }

    // ... ทำงานกับ workbook ต่อไป ...

} finally {
    if (workbook != null) {
        workbook.close();
    }
    inputStream.close();
}